连表查询都用Left Join吧 以Windows服务方式运行.NET Core程序 HTTP和HTTPS的区别 ASP.NET SignalR介绍 asp.net—WebApi跨域 asp.net—自定义轻量级ORM C#之23中设计模式
连表查询都用Left Join吧
最近看同事的代码,SQL连表查询的时候很多时候用的是Inner Join,而我觉得对我们的业务而言,99.9%都应该使用Left Join(还有0.1%我不知道在哪),我用最简单的方式来描述这两者的区别,直接看图(有点草啊):

我的做法是永远把查询主体放在左边,然后右边挨个连上要附加的信息,有则连上,没有则留null,这样思路是最清晰的。
Inner Join跟这个的区别是如果附加表找不到对应ID,那么这行记录就不会出现,我在图中用红色字体说明了这个区别。(各种Join的方式没有任何性能上的差别,只有功能上的差别)
另外一定一定要注意这几点:
- extra id这样的字段一定要指向附加表的主键。这并非SQL技术上的要求,而是我们业务逻辑的要求,如果指向的附加表的extra id不唯一,那极有可能带来你不想要的结果(通常是大量的重复),逻辑上也很难描述了。
- 如果附加表的extra id没有索引,将大大影响连表的效率。
- 如果想确保一定能带出附加信息,可以使用非空约束和外键约束。
我就简单这么定了:一定使用Left Join(除非你明确知道自己想要的是Inner Join),连表的列一定是指向另一张表的主键的。
以Windows服务方式运行.NET Core程序
在之前一篇博客《以Windows服务方式运行ASP.NET Core程序》中我讲述了如何把ASP.NET Core程序作为Windows服务运行的方法,而今,我们又遇到了新的问题,那就是:我们的控制台程序,也就是普通的.NET Core程序(而不是ASP.NET Core程序)如何以服务的方式运行呢?
这个问题我们在.NET Core之前早就遇到过,那是是.NET Framework的时代(其实距今也没多远啦),我们是用一个第三方的组件——Topshelf,来解决这个问题的,Topshelf的官网是:http://topshelf-project.com/,它的使用很简单,官网上有具体的描述,对于一个普通的控制台程序而言(通常是一个不需要图形界面的服务),开发和调试的时候,把它当做一个普通的控制台程序来使用,十分方便;而实际部署的时候,通过传入不同的命令行参数,可以使它有了新的行为:安装Windows服务、运行Windows服务、停止/重启Windows服务或者卸载Windows服务。进入跨平台的.NET Core时代之后,Topshelf自然有了支持.NET Core的版本,使用方法与之前的类似,具体在此不表了,因为接下来我们根本不打算使用它!
现在我想要的是:不要引入任何组件,不要对现在控制台程序进行任何修改(ASP.NET Core程序也是控制台程序),开发调试时候不要进行任何复杂的参数配置,一切照旧,仅仅是在部署阶段,把程序当做Windows服务去运行。——你嘚讲吼不吼?
要达到这个目标,就要借助一个神器了,此神器为NSSM,Non-Sucking Service Manager,名字有点拗口,翻译成中文就是:不嗝屁服务管理器。
NSSM的官网是:https://nssm.cc/,十分简陋,但程序功能可是非常强大和全面的,下面我来一步步演示它如何使用。
1,先构建一个简单的服务程序
构建一个简单的服务程序,程序功能描述:程序没有图形界面,仅仅是定时记录一些日志(5秒钟写一下日志),在用户按下<Ctrl>+<C>的时候,程序退出。功能明确,Okay,let's get down to work.
1. 创建一个.NET Core Application,叫MyService
2. Nuget引入Quartz和NLog.Extensions.Logging,一个用来做定时任务,另一个用来log
3. 另外,程序使用了依赖注入,还需要用Nuget引入Microsoft.Extensions.DependencyInjection
4. 给项目增加NLog.Config配置文件,内容是
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off">
<variable name="theLayout" value="${date:format=HH\:mm\:ss.fff} [${level}][${logger}] ${callsite:className=False:fileName=True:methodName=False} ${message} ${onexception:${newline}}${exception:format=Message,ShortType,StackTrace:innerFormat=Message,ShortType,StackTrace:separator=\r\n:innerExceptionSeparator=\r\n---Inner---\r\n:maxInnerExceptionLevel=5}"/>
<targets>
<target name="asyncFile" xsi:type="AsyncWrapper">
<target name="logfile" xsi:type="File" fileName="${basedir}/log/${shortdate}.log" layout="${theLayout}" encoding="UTF-8" />
</target>
<target name="debugger" xsi:type="Debugger" layout="${theLayout}" />
<target name="console" xsi:type="Console" layout="${theLayout}" />
<target name="void" xsi:type="Null" formatMessage="false" />
</targets>
<rules>
<logger name="Quartz.*" minlevel="Trace" maxlevel="Info" writeTo="void" final="true" />
<logger name="*" minlevel="Debug" writeTo="asyncFile" />
<logger name="*" minlevel="Trace" writeTo="debugger"/>
<logger name="*" minlevel="Trace" writeTo="console"/>
</rules>
</nlog>
还要注意的是这个文件必须复制到生成目录去以便程序运行时候能够加载到。
5. 增加MyServiceJobFactory.cs
using Quartz;
using Quartz.Spi;
using System;
namespace MyService {
class MyServiceJobFactory : IJobFactory {
protected readonly IServiceProvider _container;
public MyServiceJobFactory(IServiceProvider container) {
_container = container;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) {
return _container.GetService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job) {
}
}
}
6. 增加PeriodLoggingJob.cs
using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Threading.Tasks;
namespace MyService {
class PeriodLoggingJob : IJob {
private readonly ILogger<PeriodLoggingJob> _logger;
public PeriodLoggingJob(ILogger<PeriodLoggingJob> logger, IServiceProvider serviceProvider) {
_logger = logger;
}
private void DoLoggingJob() {
_logger.LogInformation("logging...");
}
public Task Execute(IJobExecutionContext context) {
try {
DoLoggingJob();
}
catch (Exception ex) { //必须妥善处理好定时任务中发生的异常
_logger.LogError(ex, "执行定时任务发生意外错误");
}
returnTask.CompletedTask;
}
}
}
7. Program.cs的完整内容如下
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
using System;
using System.Collections.Specialized;
using System.IO;
using System.Threading;
namespace MyService {
class Program {
//注册各种服务
static void RegisterServices(IServiceCollection services) {
//日志相关
services.AddSingleton<ILoggerFactory, LoggerFactory>();
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace));
//定时任务相关
services.AddSingleton<IJobFactory, MyServiceJobFactory>();
services.AddSingleton<PeriodLoggingJob>();
}
static void Main(string[] args) {
//注册退出事件处理(响应<Ctrl>+<C>)
ManualResetEvent exitEvent = new ManualResetEvent(false);
Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs e) {
e.Cancel = true;
exitEvent.Set();
};
//处理其它程序关闭事件(如kill),使得程序可以优雅地关闭
AppDomain.CurrentDomain.ProcessExit += (sender, e) => { exitEvent.Set(); };
//容器生成
ServiceCollection services = new ServiceCollection();
RegisterServices(services);
using (ServiceProvider container = services.BuildServiceProvider()) {
//日志初始化
var loggerFactory = container.GetRequiredService<ILoggerFactory>();
loggerFactory.AddNLog(new NLogProviderOptions {
CaptureMessageTemplates = true,
CaptureMessageProperties = true
});
string nlogConfigFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NLog.config");
NLog.LogManager.LoadConfiguration(nlogConfigFile);
//记录启动日志
ILogger<Program> logger = container.GetService<ILogger<Program>>();
logger.LogInformation("MyService启动.");
//定时任务配置
NameValueCollection props = new NameValueCollection { { "quartz.serializer.type", "binary" } };
StdSchedulerFactory schedulerFactory = new StdSchedulerFactory(props);
IScheduler scheduler = schedulerFactory.GetScheduler().Result;
scheduler.JobFactory = container.GetService<IJobFactory>();
//每天1:00执行APP状态更新任务
ITrigger periodLoggingJobTrigger = TriggerBuilder.Create().WithIdentity("PeriodLoggingJobTrigger")
.StartNow().WithSimpleSchedule(x=>x.WithIntervalInSeconds(5).RepeatForever()).Build();
IJobDetail checkPasswordOutOfDateJob = JobBuilder.Create<PeriodLoggingJob>().WithIdentity("PeriodLoggingJob").Build();
scheduler.ScheduleJob(checkPasswordOutOfDateJob, periodLoggingJobTrigger);
//开启定时服务
scheduler.Start();
//----------------------------------------↑↑↑ 程序开始 ↑↑↑----------------------------------------
exitEvent.WaitOne();
//----------------------------------------↓↓↓ 程序结束 ↓↓↓----------------------------------------
//定时任务结束
scheduler.Shutdown();
//记录结束日志
logger.LogInformation("MyService停止.");
}
}
}
}
这就是整个服务程序的完整内容,本来我可以提供一个更简单的程序,这里啰里啰嗦写了这么一大堆,目的还是让初学者更加清楚.NET Core的程序结构和运行方式。其中内容包括:NLog的使用、Quartz的使用、容器及依赖注入的入门例子、如何处理程序关闭事件等,也许你想问“为什么要引入Quartz,搞这么复杂,弄个Timer不行吗?”当然行,但Quartz更强大,而且更适合给大家演示容器与依赖注入的使用。
8. 试运行程序
运行这个程序,输出几条日志信息后,以<Ctrl>+<C>来结束程序的运行,这样会在程序目录下产生log目录及日志文件,文件的内容大致如下:
19:03:37.117 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:55) MyService启动. 19:03:37.637 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:42.536 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:47.535 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:49.293 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:80) MyService停止.
9. 发布程序
2,NSSM配置
1. 安装服务


其它一些操作
其实不用我说大家也应该知道了:
- nssm status MyService 查看服务状态
- nssm stop MyService 停止服务
- nssm restart MyService 重启服务
- nssm edit MyService 重新配置服务的参数
- nssm remove MyService 删除服务
其余的请自行参考nssm的使用手册。
注意事项:需要用管理员身份来执行上面这些命令,否则会出现访问拒绝的错误。
3,分享一些想法
2018年快过去了,回顾这一年来,我觉得我在公司所做的最大且重要的一件事情就是推动了.NET Core的应用,将能迁移的.NET Framework的程序都迁移至.NET Core了,为什么要这么干?最最主要的原因当然是要跨平台,原先ASP.NET开发的网站,只能运行于Windows平台,它们得依赖于IIS!Windows(作为服务器)本身就是一个非常复杂的系统,有着各种令人眼花缭乱的配置,加上IIS,就更加令人感到困惑,我同意IIS是功能强大的服务器程序,但它真的过于复杂,设计不合理,很难用,让我等菜鸟频频掉到它的坑里爬不出来。IIS并不是一个能够自由选择版本的软件,它的版本通常认为与Windows操作系统绑定,微软官方并不建议安装与Windows操作系统原生版本不一致的IIS,所以现在甚至还有公司继续在用IIS6,而各个版本的IIS的行为却不尽相同,默认IIS并不带安装ASP.NET组件,所以在Windows系统和IIS刚部署好的时候,想直接运行ASP.NET网站居然还不行,要自己去安装ASP.NET的支持,完成后还需要使用一条额外的命令来注册ASP.NET组件,另外还可能遇到稀奇古怪的问题,大多数问题可以通过安装若干个补丁解决(如ASP.NET MVC的路由不起作用导致网站无法访问的问题),而有时则不会那么顺利,你得仔细看看这些补丁是否符合当前操作系统及IIS版本,甚至操作系统的语言版本也会影响你所要安装的补丁。IIS与ASP.NET程序之间的关系也是令人很懵逼,我想让我的ASP.NET程序自始至终运行着就是做不到,尽管应用程序池里似乎有这个选项,我在StackOverflow上针对相关问题进行过讨论,有不少人顶我,但也有人说不行(我猜跟IIS版本还有关系),ASP.NET程序空闲一段时间后便被IIS踢掉——即便你的主机不差内存,你无法肯定IIS一运行你的程序就跟着跑起来,也无法肯定你的程序什么时候在运行,什么时候被踢掉,这是个类似薛定谔的猫的问题,你的ASP.NET程序就通常处于这么一种“叠加态”,你得看一看才知道确切它是否在运行,这一看,才使得程序从“叠加态”坍缩为“生态”或“死态”,且从“死态”转入“生态”还需要耗费好些时间,表现为第一次打开页面时候的长时间卡顿,跟客户演示系统,有时候会很尴尬。我曾经为了让程序不被IIS踢掉,还手工写了一个KeepAlive的小程序,定时去get我的网站的首页,实在奇葩。微软对此的解释是:IIS并不是为long-term程序设计的,你想在IIS里做一个准时的定时服务,那是相当不妥,根本不是为这种事情设计的,所以不好用不能怪我。我承认这当然是一种设计,但ASP.NET网站除了提供网页之外,跑一些后台服务也应该是很正常的吧?没办法,于是我将服务和网站分开,中间用总线沟通,听起来很cool?——其实这是一段悲伤的往事,不过说来话长,以后有机会再提了。.NET Core出现了,ASP.NET Core也和它一起到来,2.0版开始就是一个很完善的版本,我想是时候上了,这是工作量很大的差事,但为了将来更好的发展,我们必须经历这个艰难的爬坡,所幸的是现在一切都已转入正轨,我预想的目的达到了。
.NET Core的一大特点就是程序都可以独立运行,包括ASP.NET Core程序,不再依赖于IIS,我可以根据业务的需要,将系统划分为多个模块,方便开发分工和测试,这些模块甚至不需要部署在同一台主机上,极大提高了灵活性。一般来说,我还是推荐将程序部署至Linux环境,理由依旧是Linux作为服务器操作系统的使用体验远远好于Windows,Windows实在太过复杂了!但也有例外,如果遇到缺乏Linux支持技术的客户的情况,那就把程序部署到他们的Windows主机上吧,无所谓,反正.NET Core是跨平台的。
HTTP和HTTPS的区别
超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。
为了解决HTTP协议的这一缺陷,需要使用另一种协议:安全套接字层超文本传输协议HTTPS,为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。
一、HTTP和HTTPS的基本概念
HTTP:是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。
HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
二、HTTP与HTTPS有什么区别?
HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
HTTPS和HTTP的区别主要如下:
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
三、HTTPS的工作原理
我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取,所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。

客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。
(1)客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
(2)Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。
(3)客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
(4)客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
(5)Web服务器利用自己的私钥解密出会话密钥。
(6)Web服务器利用会话密钥加密与客户端之间的通信。

四、HTTPS的优点
尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案,主要有以下几个好处:
(1)使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
(2)HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
(3)HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
(4)谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。
五、HTTPS的缺点
虽然说HTTPS有很大的优势,但其相对来说,还是存在不足之处的:
(1)HTTPS协议握手阶段比较费时,会使页面的加载时间延长近50%,增加10%到20%的耗电;
(2)HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响;
(3)SSL证书需要钱,功能越强大的证书费用越高,个人网站、小网站没有必要一般不会用。
(4)SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗。
(5)HTTPS协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。最关键的,SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。
六、http切换到HTTPS
如果需要将网站从http切换到https到底该如何实现呢?
这里需要将页面中所有的链接,例如js,css,图片等等链接都由http改为https。例如:http://www.baidu.com改为https://www.baidu.com
BTW,这里虽然将http切换为了https,还是建议保留http。所以我们在切换的时候可以做http和https的兼容,具体实现方式是,去掉页面链接中的http头部,这样可以自动匹配http头和https头。例如:将http://www.baidu.com改为//www.baidu.com。然后当用户从http的入口进入访问页面时,页面就是http,如果用户是从https的入口进入访问页面,页面即使https的。
是什么?
简单来说,ASP.NET SignalR是一个开源的实时通讯(real-time)库,有了ASP.NET SignalR,我们可以在
应用场景
从介绍可以看出,ASP.NET SignalR是为实时通讯而生的,所以典型的应用场景有:
- 支付回调
- 聊天室
环境要求
一,服务器端
1,操作系统
- 服务器端支持的操作系统如下:
- Windows Server 2008 r2
- Windows Server 2012
- Windows Server 2016
注意:如果要使用WebSockets,要求操作系统Windows Server 2012+。
2,.net framework
支持.net framework 4.5+。
3,IIS
IIS 7+并且需要集成模式。
二,客户端
1,操作系统
客户端支持的操作系统为Windows 7+。
2,浏览器
- IE8+
- Firefox的所有版本
- Chrome的所有版本
- Safari的所有版本
- Opera的所有版本
注意:jQuery的版本必须>=1.6.4。
原理
1,客户端和服务端的交互如下图:

2,ASP.NET SignalR的架构图:

传输协议的协商
默认情况下,SignalR会自动协商传输协议,协商的过程如下:
-
如果浏览器是Internet Explorer 8或更早版本,则使用长轮询。
-
如果配置了JSONP(即,在启动连接时将
jsonp参数设置为true),则使用长轮询。 -
如果正在建立跨域连接(即,如果SignalR端点与托管页面不在同一个域中),则在满足以下条件时将使用WebSocket:
-
客户端支持CORS(跨源资源共享)。有关哪些客户端支持CORS的详细信息,请参阅caniuse.com上的CORS。
-
客户端支持WebSocket
-
服务器支持WebSocket
如果不满足任何这些标准,将使用长轮询。有关跨域连接的详细信息,请参阅如何建立跨域连接。
-
如果未配置JSONP且连接不是跨域连接,则如果客户端和服务器都支持,则将使用WebSocket。
-
如果客户端或服务器不支持WebSocket,则使用Server Sent Events(如果可用)。
-
如果“服务器已发送事件”不可用,则尝试使用“永久帧”。
-
如果Forever Frame失败,则使用长轮询。
入门Demo
asp.net—WebApi跨域
一、什么是跨域?
定义:是指浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。
同源策略限制了以下行为:
1、Cookie、LocalStorage和IndexDB无法读取
2、DOM和js对象无法获取
3、ajax请求无法发送
二、为什么要跨域?
跨域问题来源于JavaScript的同源策略,即只有 协议+主机名+端口号(如存在)相同,则允许相互访问。
那么跨域就是在 协议+主机名+端口号(如存在)不相同时,让其允许相互访问。
三、webapi跨域解决办法
跨域解决办法有多种, 这里我给出最近在webapi + vue 实现前后端分离项目开发中的跨域解决方案:
(1)WebApi配置文件里面添加如下配置信息即可
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="*" />
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
</customHeaders>
</httpProtocol>
</system.webServer>
(2)当遇到WebApi要开启session会话时,那么前端和后端的配置信息如下
webapi端(webapi默认是不支持session会话,需先手动设置其支持session会话)
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="http://localhost:8080" /> //此时这里就不能为 * ,要填前端项目的正确域名地址
<add name="Access-Control-Allow-Headers" value="*" />
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
<add name="Access-Control-Allow-Credentials" value="true"/>
</customHeaders>
</httpProtocol>
</system.webServer>
Vue端
● 每个ajax请求都需将 withCredentials = true
asp.net—自定义轻量级ORM
大型项目中ORM的使用已经是相当的频繁。目前.NET(C#)中比较流行的ORM框架也有很多,比如SqlSugar,Dapper,Entity Framework(EF)等。
相信很多有2年以上工作经验的园友都会使用其中一种或者几种。同时多多少少也会存在有会用却不懂其中原理的园友(我算其中一个),所以凭借
工作之余独自钻研了一段时间,现在分享下我的钻研成果。 同时也希望园内大能者指出不足之处。
在工作中,本人觉得写SQL 查询数据还是挺方便。所以这个轻量级的ORM中对于查询还是使用写SQL的方式
下图就是主要的文件:

DataFieldAttribute.cs:实体映射表字段 特性(用于标注实体类中成员属性对应数据库中的字段名和字段类型)
PropertyAttribute.cs :实体映射数据库表 特性(用于标注实体类对应数据库中哪个表)
DBCrateFactory.cs :创建数据库对象的工厂(用于创建哪种数据库对象 MS SQL 还是 ORACLE)
SQLHelper.cs :这是一个抽象函数。DBWorks文件夹下所有类都继承该抽象函数,这些子类就必须实现SQLHelper中的抽象方法同时也可以使用该抽象函数的公用方法
IWiteem.cs : 对外接口
Witeem.cs :继承并实现IWiteem接口
CommonHelper.cs :通用工具类
DBWorks文件夹下存放的是数据库操作类(因为是DEMO,所以只设置了MS SQL和ORACLE)
Enum文件夹下存放的是需要使用到的一些枚举类(ColumnKeyType.cs 字段状态, DBEnum.cs 数据库类型)
下图是接口中提供的方法:

下载地址中的代码或许还存在少部分瑕疵,在每次发现并更改过程后我及时更新。
2018-06-26 Bug:
1、SQLHelper类的ExecuteQueryPage函数

2、CommonHelper类的 DataTableToObject<T> 和 DataTableToList<T>修改成根据DataFieldAttribute特性中的字段反射对应的值
public static T DataTableToObject<T>(DataTable dt, bool IsDataField) where T : new()
{
Type type = typeof(T);
string tempName = string.Empty;
T myt = new T();
PropertyInfo[] propertys = myt.GetType().GetProperties();
PropertyInfo[] array = propertys;
int i = 0;
DataFieldAttribute attribute = null;
PropertyAttribute proAttribute = null;
while (i < array.Length)
{
PropertyInfo pi = array[i];
if (IsDataField)
{
object[] infos = null;
object[] pros = null;
infos = pi.GetCustomAttributes(typeof(DataFieldAttribute), false);
pros = pi.GetCustomAttributes(typeof(PropertyAttribute), false);
if (infos.Length > 0)
{
attribute = (DataFieldAttribute)(infos[0]);
if (pros.Length>0)
{
proAttribute = (PropertyAttribute)(pros[0]);
if (proAttribute.columnKeyType != ColumnKeyType.Extend)
{
tempName = attribute.FieldName;
}
}else
tempName = attribute.FieldName;
}
}
else
tempName = pi.Name;
if (dt.Columns.Contains(tempName))
{
if (pi.CanWrite)
{
object value = dt.Rows[0][tempName];
if (value.GetType().Equals(typeof(DateTime)))
value = Convert.ToString(value);
if (value != DBNull.Value)
pi.SetValue(myt, value, null);
}
}
i += 1;
continue;
}
return myt;
}
public static List<T> DataTableToList<T>(DataTable dt, bool IsDataField) where T : new()
{
List<T> ts = new List<T>();
Type type = typeof(T);
string tempName = string.Empty;
foreach (DataRow dr in dt.Rows)
{
T myt = new T();
PropertyInfo[] propertys = myt.GetType().GetProperties();
PropertyInfo[] array = propertys;
int i = 0;
DataFieldAttribute attribute = null;
PropertyAttribute proAttribute = null;
while (i < array.Length)
{
PropertyInfo pi = array[i];
if (IsDataField)
{
object[] infos = null;
object[] pros = null;
infos = pi.GetCustomAttributes(typeof(DataFieldAttribute), false);
pros = pi.GetCustomAttributes(typeof(PropertyAttribute), false);
if (infos.Length > 0)
{
attribute = (DataFieldAttribute)(infos[0]);
if (pros.Length > 0)
{
proAttribute = (PropertyAttribute)(pros[0]);
if (proAttribute.columnKeyType != ColumnKeyType.Extend)
{
tempName = attribute.FieldName;
}
}
else
tempName = attribute.FieldName;
}
}
else
tempName = pi.Name;
if (dt.Columns.Contains(tempName))
{
if (pi.CanWrite)
{
object value = dr[tempName];
if (value.GetType().Equals(typeof(DateTime)))
value = System.Convert.ToString(value);
if (value != DBNull.Value)
pi.SetValue(myt, value, null);
}
}
i += 1;
continue;
}
ts.Add(myt);
}
return ts;
}
2018-06-28 Bug:反射实体获取表字段时不是去特性中标注的字段名(现已修复)
1、修改工具类中的GetTableColumns函数(注释部分为旧的代码),GetTableColumns函数也做了相应的修改
public static List<string> GetTableColumns(PropertyInfo[] pis, ref List<PropertyInfo> proList, bool Isidentity = false)
{
List<string> columns = new List<string>();
object[] infos = null;
object[] fields = null;
DataFieldAttribute Fieldattribute = null;
PropertyAttribute attribute = null;
foreach (PropertyInfo pi in pis)
{
//获取此成员所有自定义特性
infos = pi.GetCustomAttributes(typeof(PropertyAttribute),false);
fields = pi.GetCustomAttributes(typeof(DataFieldAttribute), false);
if (fields.Length == 0)
{
continue;
}
Fieldattribute = (DataFieldAttribute)(fields[0]);
if (infos.Length > 0)
{
attribute = (PropertyAttribute)(infos[0]);
if (attribute == null)
{
//columns.Add(pi.Name);
columns.Add(Fieldattribute.FieldName);
proList.Add(pi);
}
else
{
switch (attribute.columnKeyType)
{
case ColumnKeyType.Extend: break;
case ColumnKeyType.Identity:
{
if (Isidentity)
{
//columns.Add(pi.Name);
columns.Add(Fieldattribute.FieldName);
proList.Add(pi);
}
}; break;
default:
{
//columns.Add(pi.Name);
columns.Add(Fieldattribute.FieldName);
proList.Add(pi);
};
break;
}
}
}
}
return columns;
}
2、MSSql.cs类 对于上述BUG做了修改
public override int Add<T>(IEnumerable<T> obj)
{
try
{
int i = 0;
int result = 0;
int success = 0;
//Type type = obj.GetType();
Type type = typeof(T);
//获取表名
string tableName = CommonHelper.GetTableName(type);
PropertyInfo[] pis = type.GetProperties();
//获取所有字段,和主键名称
List<string> columns = null;
List<PropertyInfo> proList = new List<PropertyInfo>();
columns = CommonHelper.GetTableColumns(pis,ref proList, false);
//处理是否包含主键插入
//if (isIdentity)
//{
// columns = CommonHelper.GetTableColumns(pis, true);
//}
//else
//{
// columns = CommonHelper.GetTableColumns(pis, false);
//}
foreach (T item in obj)
{
//生成SQL语句
StringBuilder sqlText = new StringBuilder();
sqlText.Append(" INSERT INTO ");
sqlText.Append(tableName);
sqlText.Append(" (");
//第一个字段
sqlText.Append(columns[0]);
//第二个起所有字段
int loop = columns.Count;
for (i = 1; i < loop; i++)
{
sqlText.Append(",");
sqlText.Append(columns[i]);
}
sqlText.Append(") VALUES (");
//第一个字段
sqlText.Append("@");
sqlText.Append(columns[0]);
//第二个起所有字段
for (i = 1; i < loop; i++)
{
sqlText.Append(",@");
sqlText.Append(columns[i]);
}
sqlText.Append(");");
//生成SqlParamter
PropertyInfo propertyInfo = null;
List<SqlParameter> paras = new List<SqlParameter>();
for (i = 0; i < loop; i++)
{
propertyInfo = proList[i];
SqlParameter para = new SqlParameter(columns[i], CommonHelper.GetSqlType(propertyInfo.PropertyType), -1);
para.Value = propertyInfo.GetValue(item);
paras.Add(para);
}
result = ExecuteNonQuery(sqlText.ToString(), CommandType.Text, paras, false);
if (result > 0)
{
success += 1;
}
}
if (conn.State == ConnectionState.Open)
{
ConColsed();
}
return success;
}
catch (Exception ex)
{
execlog.Debug(DateTime.Now.ToString() + ": Add失败,原因【" + ex.ToString() + "】");
return -1;
}
}
public override int Delete<T>(IEnumerable<T> obj)
{
try
{
int result = 0;
int success = 0;
//Type type = obj.GetType();
Type type = typeof(T);
//获取表名
string tableName = CommonHelper.GetTableName(type);
PropertyInfo[] pis = type.GetProperties();
PropertyInfo identityInfo = null;
identityInfo = CommonHelper.GetTableIdentity(pis);
string identityName = CommonHelper.GetIdentityName(pis);
if (identityInfo == null)
{
return 0;
}
if (string.IsNullOrEmpty(identityName))
{
identityName = identityInfo.Name;
}
foreach (T item in obj)
{
//生成SQL语句
StringBuilder sqlText = new StringBuilder();
sqlText.Append(" DELETE FROM ");
sqlText.Append(tableName);
sqlText.Append(" WHERE 1=1 ");
//主键筛选
sqlText.Append(" AND " + identityName);
sqlText.Append("=@" + identityName);
//生成SqlParamter
List<SqlParameter> paras = new List<SqlParameter>();
SqlParameter para = new SqlParameter(identityName, CommonHelper.GetSqlType(identityInfo.PropertyType), -1);
para.Value = identityInfo.GetValue(item);
paras.Add(para);
result = ExecuteNonQuery(sqlText.ToString(), CommandType.Text, paras,false);
if (result>0)
{
success += 1;
}
}
if (conn.State == ConnectionState.Open)
{
ConColsed();
}
return success;
}
catch (Exception ex)
{
execlog.Debug(DateTime.Now.ToString() + ": Delete失败,原因【" + ex.ToString() + "】");
return -1;
}
}
public override int Update<T>(IEnumerable<T> obj)
{
try
{
int i = 0;
int result = 0;
int success = 0;
//Type type = obj.GetType();
Type type = typeof(T);
//获取表名
string tableName = CommonHelper.GetTableName(type);
PropertyInfo[] pis = type.GetProperties();
//获取所有字段,和主键名称
string identityName = CommonHelper.GetIdentityName(pis);
//获取主键名称
PropertyInfo identityInfo = null;
identityInfo = CommonHelper.GetTableIdentity(pis);
if (identityInfo == null)
{
return 0;
}
if (string.IsNullOrEmpty(identityName))
{
identityName = identityInfo.Name;
}
List<string> columns = null;
List<PropertyInfo> proList = new List<PropertyInfo>();
//获取所有字段名称
columns = CommonHelper.GetTableColumns(pis, ref proList, true);
foreach (T item in obj)
{
//生成SQL语句
StringBuilder sqlText = new StringBuilder();
int loop = columns.Count;
sqlText.Append(" UPDATE ");
sqlText.Append(tableName);
sqlText.Append(" SET ");
//第二个起所有字段
for (i = 0; i < loop; i++)
{
//判断第一个字段是否为主键
if (columns[i] == identityName)
{
continue;
}
sqlText.Append(columns[i] + "=@" + columns[i]);
if (i < loop - 1)
{
sqlText.Append(",");
}
}
//主键筛选
sqlText.Append(" WHERE " + identityName);
sqlText.Append("=@" + identityName);
//生成SqlParamter
List<SqlParameter> paras = new List<SqlParameter>();
PropertyInfo propertyInfo = null;
for (i = 0; i < loop; i++)
{
propertyInfo = proList[i];
SqlParameter para = new SqlParameter(columns[i], CommonHelper.GetSqlType(propertyInfo.PropertyType), -1);
para.Value = propertyInfo.GetValue(item);
paras.Add(para);
}
result = ExecuteNonQuery(sqlText.ToString(), CommandType.Text, paras,false);
if (result>0)
{
success += 1;
}
}
if (conn.State == ConnectionState.Open)
{
ConColsed();
}
return success;
}
catch (Exception ex)
{
execlog.Debug(DateTime.Now.ToString() + ": Update失败,原因【" + ex.ToString() + "】");
return -1;
}
}
public override T GetModel<T>(string id)
{
int i = 0;
Type type = typeof(T);
T myT = new T();
//获取表名
string tableName = CommonHelper.GetTableName(type);
PropertyInfo[] pis = type.GetProperties();
PropertyInfo identityInfo = null;
identityInfo = CommonHelper.GetTableIdentity(pis);
string identityName = CommonHelper.GetIdentityName(pis);
if (identityInfo == null)
{
return default(T);
}
if (string.IsNullOrEmpty(identityName))
{
identityName = identityInfo.Name;
}
//获取所有字段,和主键名称
List<string> columns = null;
List<PropertyInfo> proList = new List<PropertyInfo>();
//获取所有字段名称
List<ColumnKeyType> filterList = new List<ColumnKeyType>();
filterList.Add(ColumnKeyType.Default);
filterList.Add(ColumnKeyType.Read);
filterList.Add(ColumnKeyType.Identity);
columns = CommonHelper.GetTableColumns(pis, ref proList, true);
//生成SQL语句
StringBuilder sqlText = new StringBuilder();
sqlText.Append(" SELECT ");
//第一个字段
sqlText.Append(columns[0]);
//第二个起所有字段
int loop = columns.Count;
for (i = 1; i < loop; i++)
{
sqlText.Append(",");
sqlText.Append(columns[i]);
}
sqlText.Append(" FROM ");
sqlText.Append(tableName);
sqlText.Append(" WHERE 1=1 AND ");
sqlText.Append(identityName + "=@" + identityName);
//生成SqlParamter
List<SqlParameter> paras = new List<SqlParameter>();
SqlParameter para = new SqlParameter(identityName, CommonHelper.GetSqlType(identityInfo.PropertyType), -1);
para.Value = id;
paras.Add(para);
return GetModel<T>(sqlText.ToString(), CommandType.Text, paras);
}
3、事务处理应先打开数据库连接

github下载地址:https://github.com/witeem/ASP.NET-ORM.git
C#之23中设计模式
本身打算把二十三种设计模式,总结一下。总结了几个设计模式后发现已经有博主总结的非常详细,内容丰富,我看了后也是受益良多。
大家可以参考他的博客,地址如下:
https://www.cnblogs.com/abcdwxc/archive/2007/10/30/942834.html


浙公网安备 33010602011771号