Skip to content
/ Sunny Public

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

Notifications You must be signed in to change notification settings

MackYang/Sunny

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

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等.

如:

 public class Student : BaseModel
    {
        public Student() { }

        public string StudentName { get; set; }

        public int Age { get; set; }

    }

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

 public class Student : BaseModel
    {
        public Student() { }

        private new long UpdaterId { get; set; }

    }

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

 public class Student : IDbModel
    {
        public long Id {get;set;}

        public string Name {get;set;}

    }

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

  public class Role:BaseModel
    {
        public string Name { get; set; }

        public string Remark { get; set; }

        public IList<RoleUser> RoleUsers { get; set; }
 
    }


     public class User:BaseModel
    {
        public string UserName { get; set;}
       
        public string Password { get; set; }
               
        public IList<RoleUser> RoleUsers { get; set; }
    }


 public class RoleUser:IRelationMap
    {
        public long RoleId { get; set; }

        public Role Role { get; set; }

        public long UserId { get; set; }

        public User User { get; set; }
    }

    

生成的文件如下:

 public class RoleUserMap : IEntityTypeConfiguration<RoleUser>
    {

        public void Configure(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放在该文件中

如:

 public partial class MyDbContext
    {
        public DbSet<Student> Student { get; set; }

        public DbSet<StudentAddress> StudentAddress { get; set; }

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

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

 IdTest model = new IdTest();
 model.Id = IdHelper.GenId();

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

services.AddAutoMapper();

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

    public class ResponseMapperConfig : Profile
    {
        public ResponseMapperConfig()
        {

            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,然后在需要转换的地方获取转换后的类型

IMapper mapper;

public SomeClass(IMapper mapper)
{
    this.mapper=mapper;
}

public void SomeMethod()
{
    Buyer getBuyer = mapper.Map<Buyer>(seller);
}

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

 public class JobB : IJobEntity
    {

        public string JobName => "this job B Name";

        public string Describe => "this job B Describe";

        IStudentServic studentServic;

        public JobB(IStudentServic studentServic)
        {
            this.studentServic = studentServic;

        }


        public async Task ExecuteAsync(IJobExecutionContext jobContext)
        {
            Console.WriteLine(jobContext.JobDetail.JobDataMap["pxxx"]);//使用了配置中传来的参数,参数的名称要和配置里的一样
            Console.WriteLine( (await studentServic.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会在配置的时间运行,届时会将运行时间及任务运行耗时等信息写入到日志.

 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, ISchedulerFactory schedulerFactory)
        {
            app.InitServiceProvider();
            app.EnableJob(Configuration, schedulerFactory);
        }

在StartUp.cs中注册Fluent验证:

services.AddMvcCore()
                .AddFluentValidation()

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

 public class CustomerValidator : Validator<Customer>
    {
        public CustomerValidator()
        {
            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");
        }

        private bool BeAValidPostcode(string postcode)
        {
            return postcode == "123";
            // custom postcode validating logic goes here
        }
    }

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

 /// <summary>
        /// 带返回值的成功场景测试,测试模型验证
        /// </summary>
        /// <returns></returns>
        [HttpPost("Get2")]
        public Result<A> Get2(Customer customer)
        {

            return this.Success(new A { 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", new Info { Title = "ApiDemo Project Swagger API", Version = "v1" });
                // 为 Swagger JSON and UI设置xml文档注释路径
                var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径)
                var xmlPath = 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.

 public async Task<Student> BizExceptionTest()
        {
            throw new BizException("订单不存在,这是一个测试抛出的业务异常");
        }

前端收到的数据:

{
    "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中的任意接口,系统启动时会自动注册依赖关系,在使用时可通过构造函数注入的方式获取您类或接口的实例.

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

  public interface IStudentServic:IScoped
    {

        Task<Student> GetStudent();

        Task<Student> GetStudent2();
    }


    public class SomeoneClass : IScoped
    {

        public string SomeoneMethod()
        {
            return "hello this is di class";
        }
    }

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

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

 var s = DiHelper.CreateInstance<SomeoneClass>();
 var x = 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 =>
            {
                var configOption = 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")]
        public async Task<Result<dynamic>> GetRedis()
        {
            cache.SetString("aaa", "A杨家勇A");

            cache.Set("customer", new Customer());

            await cache.SetAsync("customerAsync", new Customer() { Address = "Async" });

            var cus = cache.Get<Customer>("customer");

            var cusAsync = await cache.GetAsync<Customer>("customerAsync");

            return this.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(new LongConverter());
                    x.Converters.Add(new DecimalConverter());
                    x.DateFormatString = "yyyy-MM-dd HH:mm:ss";
                    x.ContractResolver = new CamelCasePropertyNamesContractResolver();
                })

为了便于前端解析,只要成功调用到我们的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")]
        public async Task<Result<Student>> GetOld()
        {
            var y = await studentServic.GetStudent2();
             return this.Success(y);
             
         }

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

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

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

 /// <summary>
        /// 不带返回值的成功场景测试
        /// </summary>
        /// <returns></returns>
        // GET api/values
        [HttpGet("Get1")]
        public Result Get1()
        {

            return this.Success();
        }

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

 [HttpGet("GetDynamic")]
public Result<dynamic> GetDynamic()
{
    A a= from db...;
    this.SuccessDynamic(a.Extend(new { EnumCn = a.LocalType.GetDescribe() }));
}

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

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

        /// <summary>
        /// 带返回值,且值为动态扩展对象的场景测试,分页测试
        /// </summary>
        /// <returns></returns>
        [HttpPost("pageTest")]
        public Result<PageData<dynamic>> pageTest(PageInfo pageInfo)
        {
            var pageList = db.IdTest.Pagination(pageInfo);
            //让列表中返回的每一项都有At和Sort属性         
            return this.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"));

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

        MailOption mailOption;
        IpInfoQueryOption ipQueryOption;
        
        public ValuesController(IOptions<MailOption> mailOption,IOptions<IpInfoQueryOption> ipQueryOption)
        {

            this.mailOption = mailOption.Value;
            this.ipQueryOption = ipQueryOption.Value;
        }

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

        [HttpGet("IpQuery")]
        public Result<IPInfo> IpQuery()
        {
            //var ip=NetHelper.GetClientIP(this.HttpContext);
            var ip = "171.214.202.111";
            return this.Success(NetHelper.QueryIpInfo(ip, ipQueryOption));
        }

        [HttpGet("MailTest")]
        public  Result<string> MailTest()
        {
            MailInfo mailInfo = new MailInfo();
            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);
            return this.Success("ok");
        }

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

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


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


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

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


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


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


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

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

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

About

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

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages