Skip to content

Latest commit

History

137 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Sunny

这是一个基于AspNetCore+EFCore的快速开发框架(NetCore2.1),用更少的代码去实现目标,希望可以为更多的人节省时间.

目前已集成和实现的内容


未来打算集成和实现的内容

  • 发送短信
  • Lucence
  • RabbitMq
  • 支付相关(微信,支付宝)
  • SignalR
  • 微信登录
  • 基于Vue的后台管理UI
  • Sso

使用文档

快速开始

(备注:文档中的斜体部分是作为建议,不是必须的.)



创建一个AspNetCore的WebApi项目,从Nuget中搜索SunnyApi并引用

建议将UseDemo\ApiDemo中的Program.cs类定义部分复制到您的Program.cs中.

请将UseDemo\ApiDemo中的appsettings.json文件复制到您的项目中进行替换,并将StartUp.cs类定义部分复制到您的StartUp.cs中,在报红的地方按Alt+Enter引入对应的命名空间.

将StartUp.cs中的MyDbContext部分换成您项目中自定义的数据库上下文.



创建一个NetCore的类库项目,作为您的数据库访问层,并在该项目中引入Sunny.Repository的Nuget包

新建一个类作为您的数据库上下文,如MyDbContext,将RepositoryDemo中的MyDbContext.cs类定义部分的代码复制到您的类中.

记得将FluentApiTools.ApplyDbModelFluentApiConfig(modelBuilder, "RepositoryDemo");中的"RepositoryDemo"修改成您Repository项目的名称,以便接下来应用FluentApi的DbModel配置.

新建一个类作为您设计时的上下文工厂,并请将DesignTimeDbContextFactory中的类定义部分代码复制到该类中, 用于将DbModel的修改应用到数据库中.

新建一个DbModel文件夹,内部结构可参照RepositoryDemo项目中的DbModel.

Model用于存放您定义的DbModel类ModelConfig用于存放FluentApi的配置,这个文件夹中的文件是自动生成的,主要作用是将DbModel中的大写转成数据库中的下划线,配置id等,字段的内容长度等,通常生成完成后,需要手动修改某些字段长度,生成部分具体操作请见使用T4模板自动生成DbModel的FluentApi配置RelationMap用于存放您DbModel关系间的配置,在遵循微软EF约定的默认关系规则情况下,通常情况不需要手动配置

建议您的DbModel继承自BaseModel,BaseModel里包含了Id,CreaterId,CreateTime,UpdaterId,UpdateTime等.

如:

publicclassStudent:BaseModel{publicStudent(){}publicstringStudentName{get;set;}publicintAge{get;set;}}

如果BaseModel里的某写字段你用不到,可以采用 private new 的形式覆盖,这样就不会在数据库中生成对应的字段,比如:

publicclassStudent:BaseModel{publicStudent(){}privatenewlongUpdaterId{get;set;}}

当然,你也可以不继承自BaseModel,而是继承自IDbModel,这样的话所有的字段自己定义,继承自IDbModel是让T4模板自动生成FluentApi的配置文件(生成操作后边会讲)):

publicclassStudent:IDbModel{publiclongId{get;set;}publicstringName{get;set;}}

另外,通过继承IRelationMap,可以通过T4模板生成多对多的关系配置如:

publicclassRole:BaseModel{publicstringName{get;set;}publicstringRemark{get;set;}publicIList<RoleUser>RoleUsers{get;set;}}publicclassUser:BaseModel{publicstringUserName{get;set;}publicstringPassword{get;set;}publicIList<RoleUser>RoleUsers{get;set;}}publicclassRoleUser:IRelationMap{publiclongRoleId{get;set;}publicRoleRole{get;set;}publiclongUserId{get;set;}publicUserUser{get;set;}}

生成的文件如下:

publicclassRoleUserMap:IEntityTypeConfiguration<RoleUser>{publicvoidConfigure(EntityTypeBuilder<RoleUser>builder){builder.ToTable("role_user");builder.HasKey(t =>new{t.RoleId,t.UserId});builder.HasOne(t =>t.Role).WithMany(x =>x.RoleUsers).HasForeignKey(t =>t.RoleId);builder.HasOne(t =>t.User).WithMany(x =>x.RoleUsers).HasForeignKey(t =>t.UserId);}}

如果您的字段中有名为IsDelete的字段,为自动生成默认的查询过滤器,如builder.ToTable("your_table").HasQueryFilter(x => x.IsDelete);

建议新建一个DbSet.cs文件,作为MyDbContext的分部类,把所有的DbSet放在该文件中

如:

publicpartialclassMyDbContext{publicDbSet<Student>Student{get;set;}publicDbSet<StudentAddress>StudentAddress{get;set;}publicDbSet<IdTest>IdTest{get;set;}}


使用T4模板自动生成DbModel的FluentApi配置

创建一个NetCore的控制台应用程序项目,用于生成T4模板,并在该项目中引用您的Repository项目,以及引用Sunny.TemplateT4的Nuget包

将UseDemo/TemplateT4Demo下的Program.cs类定义部分复制到您的Program.cs中替换后修改下图所示的地方为您的Repository项目名称.

运行该项目,如果没有指定输出路径,默认输出到D:\SunnyFramework\Output\DbConfig\下,打开该目录,把生成的文件复制到您Repository项目中的ModelConfig文件夹下按实际需要做相应修改.

DbModel写好之后,再用T4模板生成FluentApi配置,就可以通过Add-Migration xx 和 Update-Database将DbModel应用到数据库中了.

建议创建一个Service项目对Repository中的数据库层进行业务逻辑封装,再提供给Api层调用.


在StartUp.cs文件Configure方法中进行初始化

IdHelper.InitSnowflake(Configuration.GetSection("SunnyOptions:SnowflakeOption").Get<SnowflakeOption>());

然后在您要使用的地方进行调用

IdTestmodel=newIdTest();model.Id=IdHelper.GenId();

在StartUp.cs文件ConfigureServices方法中启用AutoMapper

services.AddAutoMapper();

创建一个类,继承自Profile类,并在构造器里配置各类型的转换关系

publicclassResponseMapperConfig:Profile{publicResponseMapperConfig(){CreateMap<IdTest,Customer>().ForMember(cus =>cus.LocalType, opt =>opt.MapFrom(id =>id.requestType)).ReverseMap();//手动指定字段映射关系//.ForMember(cus => cus.LocalType, opt => opt.MapFrom(id => id.requestType))//.ReverseMap()映射反向转换CreateMap<Buyer,Seller>().ReverseMap();}}

在要使用的类里通过构造函数注入一个IMapper类型的对象mapper,然后在需要转换的地方获取转换后的类型

IMappermapper;publicSomeClass(IMapper mapper){this.mapper=mapper;}publicvoidSomeMethod(){BuyergetBuyer=mapper.Map<Buyer>(seller);}

在Api项目下创建一个类,实现IJobEntity接口

publicclassJobB:IJobEntity{publicstringJobName=>"this job B Name";publicstringDescribe=>"this job B Describe";IStudentServicstudentServic;publicJobB(IStudentServicstudentServic){this.studentServic=studentServic;}publicasyncTaskExecuteAsync(IJobExecutionContextjobContext){Console.WriteLine(jobContext.JobDetail.JobDataMap["pxxx"]);//使用了配置中传来的参数,参数的名称要和配置里的一样Console.WriteLine((awaitstudentServic.GetStudent()).StudentName);}}

在appsetting.json中配置任务:

"JobOption": [
{
//Job所的的类名称"JobClassName": "JobB",
//Job所属的组,同一组中不能有2个相同的任务"JobGroup": "group1",
//Job在什么时候运行,用Cron表达式"RunAtCron": "*/55 * * * * ?",
//Job的参数,没有可以不写"Args": {
//参数名字和你在任务中写的要相同"pxxx": "kkk",
"nnn": 123
}
},
{
//Job所的的类名称"JobClassName": "JobA",
//Job在什么时候运行,用Cron表达式"RunAtCron": "*/59 * * * * ?"
}
]

在StartUp.cs的ConfigureServices方法中注册任务服务

services.AddSingleton<ISchedulerFactory,StdSchedulerFactory>();//注册ISchedulerFactory的实例。

在StartUp.cs的Configure方法中启用Job,启用后,Job会在配置的时间运行,届时会将运行时间及任务运行耗时等信息写入到日志.

publicvoidConfigure(IApplicationBuilderapp,IHostingEnvironmentenv,ILoggerFactoryloggerFactory,ISchedulerFactoryschedulerFactory){app.InitServiceProvider();app.EnableJob(Configuration,schedulerFactory);}

在StartUp.cs中注册Fluent验证:

services.AddMvcCore().AddFluentValidation()

在Api项目中创建一个类,继承自Validator,如:

publicclassCustomerValidator:Validator<Customer>{publicCustomerValidator(){RuleFor(x =>x.Surname).NotEmpty();RuleFor(x =>x.Forename).NotEmpty().WithMessage("PleasFFe specify a first name");RuleFor(x =>x.Discount).NotEqual(0).When(x =>x.HasDiscount);RuleFor(x =>x.Address).Length(20,250);RuleFor(x =>x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");}privateboolBeAValidPostcode(stringpostcode){returnpostcode=="123";// custom postcode validating logic goes here}}

在Api中直接使用实体Customer,进入方法之前会先对customer验证,如果验证不通过不会进入方法内部,会返回相应的提示信息:

/// <summary>/// 带返回值的成功场景测试,测试模型验证/// </summary>/// <returns></returns>[HttpPost("Get2")]publicResult<A>Get2(Customercustomer){returnthis.Success(newA{FullName="AbcYH",Age=123.123456789m,MFF=long.MaxValue});}

在写参数时可以不显示的声明[FromBody],[FromQuery],[FormHeader]等,默认是[FromBody],如果不是通过body传参,可以显示声明传参方式.


请设置您Api项目的生成选项,输出xml以便SwaggerUI中能看到Api的注释内容

在StartUp.cs的ConfigureServices方法中加入以下代码:

services.AddSwaggerGen(c =>{c.SwaggerDoc("v1",newInfo{Title="ApiDemo Project Swagger API",Version="v1"});// 为 Swagger JSON and UI设置xml文档注释路径varbasePath=Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径)varxmlPath=Path.Combine(basePath,"ApiDemo.xml");c.IncludeXmlComments(xmlPath);});

并将下图中的xml文件为您项目的xml文件名称

在StartUp.cs的Configure方法的最后加入以下代码:

app.UseSwaggerUI(c =>{c.SwaggerEndpoint("/swagger/v1/swagger.json","My API V1");});

运行Api项目,即可访问Swagger文档,如"https://localhost:44381/swagger"


在StartUp.cs的Configure方法中加入以下代码(通常在所有中间件前加入,以便捕获所有异常):

if(env.IsDevelopment()){app.UseDeveloperExceptionPage();}else{app.UseMiddleware<ErrorHandlingMiddleware>();}

这样在非开发环境中,都会使用异常处理中间件,将异常写入到日志中,并向客户端显示友好信息.

关于业务异常的处理:

在开发过程中,通常将业务逻辑封装在Service层中供Api层调用,那么当Service层内的数据不符合逻辑时,直接抛出业务异常,经中间件处理后将具体的信息返回给前端,但Code的枚举值不是异常的-1,而是业务失败的1.

publicasyncTask<Student>BizExceptionTest(){thrownewBizException("订单不存在,这是一个测试抛出的业务异常");}

前端收到的数据:

{
"code": 1,
"msg": "订单不存在,这是一个测试抛出的业务异常",
"data": null
}

在实际业务场景中,很多Api都是在访问前要验证是否登录,我们可以在appsetting.json中配置以指定路径开头的api需要验证后才能访问,如:

"TokenValidateOption": {
//将根据该值来从HttpHeader中存取对应的token值,默认就是"token""TokenKey": "token",
// 以此开头的API都需要验证Token,如果不需要可不配置"AuthApiStartWith": "/api"
},

在StartUp.cs的ConfigureServices方法中加入以下代码:

services.Configure<TokenValidateOption>(Configuration.GetSection("SunnyOptions:TokenValidateOption"));

在StartUp.cs的Configure方法中加入以下代码(通常在app.UseMvc()中间件前):

app.UseMiddleware<TokenValidateMiddleware>();

这样一来,不用在每个需要登录的方法上加标注,中间件会从header中获取token的值,并以值作为key去缓存查询是否存在(需要您在登录成功的方法里将对应的token值写入到缓存,框架中默认的是redis),如果存在即验证通过.

对于不用验证的api,如注册,注销,发验证码等,以非/api开头的路径即可,如"/unAuth/api".


为了方便集中管理和查询日志,框架提供了网络日志,写入后到指定的url上可查询您的日志.

在appsetting.json中配置日志选项:

"NetLoggerOption": {
"Url": "http://test.log.loc-mall.com/Api/AddLog",
//每个业务系统配置自己的ID,请到http://test.log.loc-mall.com/ui/pages/apply.aspx申请,并妥善保存,//写入的日志在这里查看http://test.log.loc-mall.com/ui/pages/log.aspx?systemid=xxxxxxxx//生产环境去掉test.即可,或者到https://github.com/MackYang/LogService.git下来部署载自己的日志系统"SystemId": "b06b7e4d-bbce-11e8-98ca-00163e063309",
//存放离线日志的目录,当网络日志记录失败时,会将日志以yyyy-MM-dd.txt写到该目录下"OfflineLogPath": "D:\\SunnyFramework\\OfflineLog"
},

在StartUp.cs的Configure方法中加入以下代码:

loggerFactory.AddNetLoggerUseDefaultFilter(Configuration.GetSection("SunnyOptions:NetLoggerOption").Get<NetLoggerOption>());

这样便会记录所有level>=info的分类为非microsoft开头的日志,分类以microsoft开头的日志在>=warning以上时才记录.

查询日志,访问指定的url即可,如:http://test.log.loc-mall.com/ui/pages/log.aspx?systemid=xxxxx

如果网络日志写入失败(通常是因为网络原因),会将日志写入到C:\SunnyFramework\OfflineLog下以日期作为文件名的txt文件里,如:


在StartUp.cs的ConfigureServices方法中加入以下代码:

DiHelper.AutoRegister(services);

当您的类或接口继承自ISingleton,IScoped,ITransient中的任意接口,系统启动时会自动注册依赖关系,在使用时可通过构造函数注入的方式获取您类或接口的实例.

首先我们的类或接口继承自以上任意接口:

publicinterfaceIStudentServic:IScoped{Task<Student>GetStudent();Task<Student>GetStudent2();}publicclassSomeoneClass:IScoped{publicstringSomeoneMethod(){return"hello this is di class";}}

然后在我们要使用的地方通过构造函数注入:

如果您不想使用构造函数的注入方式,也可以在要使用的地方直接创建实例:

vars=DiHelper.CreateInstance<SomeoneClass>();varx=DiHelper.GetService<IStudentServic>();

在appsetting.json中配置redis选项:

"RedisOptions": {
//连接字符串"ConnectionString": "127.0.0.1:6543",
//调用方实例名称,redis中的key会自动以设置的字符串开头,用以标识是哪台机器存入的key"InstanceName": "api_",
//默认的滑动过期时间多少秒,为了防止缓存击穿,实际存入时会在该时间上加10秒类的随机数"DefaultSlidingExpiration": 600
}

在StartUp.cs的ConfigureServices方法中加入以下代码:

services.AddDistributedRedisCache(options =>{varconfigOption=Configuration.GetSection("SunnyOptions:RedisOptions").Get<RedisOption>();options.Configuration=configOption.ConnectionString;options.InstanceName=configOption.InstanceName;IDistributedCacheExtend.DefaultSlidingExpiration=configOption.DefaultSlidingExpiration;});

通过构造函数注入一个IDistributedCache的实例cache后即可调用:

/// <summary>/// Redis测试/// </summary>/// <returns></returns>[HttpGet("GetRedis")]publicasyncTask<Result<dynamic>>GetRedis(){cache.SetString("aaa","A杨家勇A");cache.Set("customer",newCustomer());awaitcache.SetAsync("customerAsync",newCustomer(){Address="Async"});varcus=cache.Get<Customer>("customer");varcusAsync=awaitcache.GetAsync<Customer>("customerAsync");returnthis.SuccessDynamic(new{Cus=cus,CusAsync=cusAsync});}

当我们以js作为api的前端应用时,会发现Long,Decimal类型会产生精度丢失的现象,DateTime的日期中会带"T"的现象.

为此,我们将针对这三种类型的json格式化作加工处理,Long,Decimal转为字符串,前端传给我们的时候自动转为对应的类型.

DateTime类型我们显式的设置它的字符串格式.

在StartUp.cs的ConfigureServices方法中加入以下代码:

services.AddMvcCore().AddJsonFormatters(x =>{x.Converters.Add(newLongConverter());x.Converters.Add(newDecimalConverter());x.DateFormatString="yyyy-MM-dd HH:mm:ss";x.ContractResolver=newCamelCasePropertyNamesContractResolver();})

为了便于前端解析,只要成功调用到我们的Api时,每个返回的结果中都会由code,msg,data这三个元素组成.

code:为0时表示操作成功,为1时表示操作失败,为-1时表示操作异常.

msg:默认是"操作成功",当code不为0时,msg表示具体的失败原因,code为-1时显示"我们已经收到此次异常信息,将尽快解决!"

data:当code为0时表示返回的具体数据,code为非0时通常为null.

另外,为了我们在维护Api时一眼就能看出给前端返回的数据内容,建议将返回的数据项创建为一个类,在Api的返回结果中显示的指定该类Result,如:

[HttpGet("GetOld")]publicasyncTask<Result<Student>>GetOld(){vary=awaitstudentServic.GetStudent2();returnthis.Success(y);}

这样的好处是,在我们看Api时,不会一堆Api都返回object.

当操作失败时,我们要用 return this.Fail("这是原因");向前端返回失败的原因.

对于没有数据需要返回给前端,只要返回操作状态时,我们Api定义的返回值非泛型版本Result

/// <summary>/// 不带返回值的成功场景测试/// </summary>/// <returns></returns>// GET api/values[HttpGet("Get1")]publicResultGet1(){returnthis.Success();}

通常我们会遇到这样的场景...我们要将某个枚举的中文意思返回给前端,但又不想新建一个类型,于是框架里有了一个偷懒的办法:

[HttpGet("GetDynamic")]publicResult<dynamic>GetDynamic(){Aa=fromdb...;this.SuccessDynamic(a.Extend(new{EnumCn=a.LocalType.GetDescribe()}));}

这样做的缺点就是不能通过Api的方法签名直观的看出返回的类型.

某些场景可能需要将列表中的每一项都扩展一些额外的属性返回,框架针对List和PageData扩展了一个方法ToDynamic,以解决这样的场景:

/// <summary>/// 带返回值,且值为动态扩展对象的场景测试,分页测试/// </summary>/// <returns></returns>[HttpPost("pageTest")]publicResult<PageData<dynamic>>pageTest(PageInfopageInfo){varpageList=db.IdTest.Pagination(pageInfo);//让列表中返回的每一项都有At和Sort属性 returnthis.Success(pageList.ToDynamic(x =>x.Extend(new{At=DateTime.Now,Sort=DateTime.Now.Millisecond})));}

分页测试:


首先在appsettings.json中配置邮件发送和Ip查询选项:

//邮件配置"MailOption": {
// 邮件服务器地址"EmailHost": "smtp.ym.163.com",
// 用户名"EmailUserName": "you user name",
// 密码"EmailPassword": "you password",
// IP白名单列表,在列表中的IP发邮件前不执行检查事件"IPWhiteList": [ "127.0.0.1" ]
},
//查询IP信息的配置"IpInfoQueryOption": {
//IP查询的API"ApiUrl": "http://www.ip.cn/index.php?ip="
},

然后在StartUp.cs的ConfigureServices方法中加入以下代码:

services.Configure<MailOption>(Configuration.GetSection("SunnyOptions:MailOption"));services.Configure<IpInfoQueryOption>(Configuration.GetSection("SunnyOptions:IpInfoQueryOption"));

在使用到的地方通过构造函数注入配置项:

MailOptionmailOption;IpInfoQueryOptionipQueryOption;publicValuesController(IOptions<MailOption> mailOption,IOptions<IpInfoQueryOption> ipQueryOption){this.mailOption=mailOption.Value;this.ipQueryOption=ipQueryOption.Value;}

再调用NetHelper的相关方法即可,如:

[HttpGet("IpQuery")]publicResult<IPInfo>IpQuery(){//var ip=NetHelper.GetClientIP(this.HttpContext);varip="171.214.202.111";returnthis.Success(NetHelper.QueryIpInfo(ip,ipQueryOption));}[HttpGet("MailTest")]publicResult<string>MailTest(){MailInfomailInfo=newMailInfo();mailInfo.Content="hello";mailInfo.OperaterID="yh";mailInfo.OperaterIP=NetHelper.GetClientIP(this.HttpContext);mailInfo.Title="this is test mail";mailInfo.ToMail="someone@qq.com";NetHelper.AsyncSendEmail(mailInfo,mailOption);returnthis.Success("ok");}

字符串扩展部分的内容就不一一列举了,请通过"".来查看相关的方法,枚举主要扩展了一个GetDescribe()方法来获取DescriptionAttribute属性标注的内容,如果没有标注则返回枚举项的或称.

另外字符串的辅助类请用StringHelper.点出来看.


图片的处理,如缩放,加水印,生成验证码图片等方法都在ImageHelper中,直接调用相关方法即可使用.


Xml文件的操作请见XMLHelper类中的方法.

文本文件的操作请见FileHelper类中的方法.


加解密相关的操作请见SecurityHelper中提供的方法,目前提供了MD5,SHA1加密以及Des的加解密.


Base64的序列化操作请见SerializeHelper中提供的方法.


使用文档不断完善中,如果在使用中遇到问题,可以查看UseDemo或到技术交流QQ群852498368寻求帮助.

如果该框架有帮助到您,请送上您的小星星哦.

如果您愿意贡献代码,请尽情的Fork吧,希望更多的人因为我们的存在而让生活变得更加美好!

About

这是一个.NetCore+EF的快速开发框架,用更少的代码去实现目标,希望可以为更多的人节省时间

Resources

Stars

54 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages