pinia是一个vue的状态存储库,你可以使用它来存储、共享一些跨组件或者页面的数据,使用起来和vuex非常类似。pina相对Vuex来说,更好的ts支持和代码自动补全功能。本篇随笔介绍pinia的基础用法以及持久化存储的一些用法,供参考学习。

pinia在2019年11月开始时候是一个实验项目,目的就是重新设计一个与组合API匹配的vue状态存储。基本原则和原来还是一样的,pinia同时支持vue2和vue3,且不要求你必须使用Vue3的组合API。不管是使用vue2或者vue3,pinia的API是相同的,文档是基于vue3写的。

Pinia 是 Vuex4 的升级版,也就是 Vuex5; Pinia 极大的简化了Vuex的使用,是 Vue3的新的状态管理工具;Pinia 对 ts的支持更好,性能更优, 体积更小,无 mutations,可用于 Vue2 和 Vue3;Pinia支持Vue Devtools、 模块热更新和服务端渲染。

1、pinia的安装和使用

安装pinia(
https://pinia.vuejs.org/
)

npm install pinia

在main.j或者main.ts中引入使用

import { createPinia } from 'pinia'app.use(createPinia())

下面就是使用pinia的一个例子。这样你就创建了一个状态存储。

//stores/counter.js
import { defineStore } from 'pinia'export const useCounterStore= defineStore('counter', {
state: ()
=>{return { count: 0}
},
//也可以这样定义状态 //state: () => ({ count: 0 }) actions: {
increment() {
this.count++},
},
})

在组件中使用:

import { useCounterStore } from '@/stores/counter'exportdefault{
setup() {
const counter
=useCounterStore()

counter.count
++ //编辑器会有代码提示 counter.$patch({ count: counter.count + 1})//也可以使用action来代替 counter.increment()
},
}
如果你不是很喜欢setup函数和组合API,pinia也有类似vuex的map的功能。你可以用上面的方式定义你的store,但是使用时用mapStores(), mapState(),或者 mapActions():
const useCounterStore = defineStore('counter', {
state: ()
=> ({ count: 0}),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++}
}
})

const useUserStore
= defineStore('user', {//... })

export
default{
computed: {
//其他计算属性 //... //可以使用 this.counterStore 和 this.userStore获取 ...mapStores(useCounterStore, useUserStore)//可以使用 this.count 和this.double获取 ...mapState(useCounterStore, ['count', 'double']),
},
methods: {
//可以使用 this.increment()调用 ...mapActions(useCounterStore, ['increment']),
},
}

与vue4之前的版本相比,pinia的API是有很多不同的,即:

  • 去掉了mutation。因为好多人认为mutation是多余的。以前它方便devtools集成,现在这不是个问题了。
  • 不用在写复杂的ts类型包装,所有的都是有类型的,API设计的都是尽量符合ts的类型推断
  • 不再使用一个莫名其妙的字符串了,只需要导入一个函数,调用他们就行了,同时还有代码自动补全
  • 不需要动态添加store了,因为它们现在本来就是动态。如果你想,你随时可以手动去写一个store。
  • 没有复杂的嵌套模块了。你仍然可以在一个store中导入其他的store来实现嵌套模块,但是pinia还是推荐使用一个扁平的结构。但是即使你使用循环依赖也没关系。
  • 不再需要命名空间了。因为现在store本来就是扁平结构了。你也可以理解为所有的store本来就有命名空间了。
你的应用中的全局数据需要保存在store中。在很多地方你都要使用这些数据,比如说,用户信息需要在导航栏中显示,也需要在个人中心显示。还有些数据,需要暂存起来,比如一个需要分好几页填写的表单。
在pinia中,store是通过defineStore()方法定义的,它的第一个参数就是一个唯一的名字:
import { defineStore } from 'pinia'export const useStore= defineStore('main', {//other options...
})

上面只是定义了store,在setup函数中调用了useStore()时,才会创建store:

import { useStore } from '@/stores/counter'exportdefault{
setup() {
const store
=useStore()return{//你可以返回store这个对象,然后就可以在template中使用了 store,
}
},
}

在store实例化以后,你就可以调用到store中定义的state、getters和actions了。为了让解构的值还保持响应式,你需要用到storeToRefs()方法。它会给响应式的数据创建ref。

import { storeToRefs } from 'pinia'exportdefaultdefineComponent({
setup() {
const store
=useStore()//`name` 和 `doubleCount` 是响应式的 //插件增加的属性也会创建ref //但是会自动跳过action或者不是响应性的属性 const { name, doubleCount } =storeToRefs(store)return{
name,
doubleCount
}
},
})

默认情况下,你可以在store实例上直接获取或者修改state:

const store =useStore()
store.counter
++

也可以调用$reset()方法来把state恢复为初始值:

const store =useStore()
store.$reset()

除了直接修改store里的值store.counter++,你也可以是用$patch方法。你可以同时修改多个值:

store.$patch({
counter: store.counter
+ 1,
name:
'Abalam',
})

或者$patch接收一个函数作为参数,来简化改变数组的写法:

store.$patch((state) =>{
state.items.push({ name:
'shoes', quantity: 1})
state.hasChanged
= true})

2、pinia的持久化存储处理

你可以用$subscribe()来侦听state的改变,持久化一般存储在
localStorage和sessionStorage。

localStorage和sessionStorage差别

localStorage和sessionStorage一样都是用来存储客户端临时信息的对象。

他们均只能存储字符串类型的对象(虽然规范中可以存储其他原生类型的对象,但是目前为止没有浏览器对其进行实现)。

localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。

sessionStorage生命周期为当前窗口或标签页,一旦窗口或标签页被永久关闭了,那么所有通过sessionStorage存储的数据也就被清空了。

不同浏览器无法共享localStorage或sessionStorage中的信息。相同浏览器的不同页面间可以共享相同的 localStorage(页面属于相同域名和端口),但是不同页面或标签页间无法共享sessionStorage的信息。这里需要注意的是,页面及标 签页仅指顶级窗口,如果一个标签页包含多个iframe标签且他们属于同源页面,那么他们之间是可以共享sessionStorage的。

JSON对象提供的parse和stringify将其他数据类型转化成字符串,再存储到storage中就可以了,操作的方式:

存:

var obj = {"name":"xiaoming","age":"16"}

localStorage.setItem("userInfo",JSON.stringify(obj));

取:

var user = JSON.parse(localStorage.getItem("userInfo"))

删除:

localStorage.remove("userInfo);

清空:

localStorage.clear();

pnia 使用订阅机制subscribe来实现数据的持久化存储的代码如下所示。

const instance =useMainStore();//订阅数据变化,变化时存储 instance.$id 这是storeId
instance.$subscribe((mutation, state) =>{
localStorage.setItem(instance.$id, JSON.stringify(state));
});
//init 初始的时候获取 const val =localStorage.getItem(instance.$id);if(val) {
instance.$state
=JSON.parse(val);
}

也可以通过watch实现

watch(
pinia.state,
(state)
=>{//persist the whole state to the local storage whenever it changes localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep:
true}
)

但是需要注意,这种方式持久化会提示pinia未安装挂载,所以需要在pinia挂载后再调用,这里可以将它封装成方法导出,在挂载后调用

xport const initStore = () =>{
const instance
=useMainStore();//订阅数据变化,变化时存储 instance.$id 这是storeId instance.$subscribe((mutation, state) =>{
localStorage.setItem(instance.$id, JSON.stringify(state));
});
//init 初始的时候获取 const val =localStorage.getItem(instance.$id);if(val) {
instance.$state
=JSON.parse(val);
}

}
默认情况下,state侦听会和组件绑定在一起(如果store是在组件的setup中)。这意味着,当组件卸载时,侦听会自动被移除。如果你需要在组件被卸载时,侦听仍然保持,需要给$subscribe()方法传递第二个参数true:
export default{
setup() {
const someStore
=useSomeStore()//组件卸载后,侦听也会有 someStore.$subscribe(callback, true)//... },
}

或者watch状态的变化

watch(
pinia.state,
(state)
=>{//在state改变时,保存在localStorage中 localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep:
true}
)

3、使用pinia插件持久化存储

pinia plugin persist官方网站:
pinia-plugin-persist

持久化存储也可以通过安装插件的方式,安装 pinia-plugin-persist 来实现。

npm i pinia-plugin-persist --save

使用main.js

import { createPinia } from 'pinia'import piniaPluginPersist from'pinia-plugin-persist'const store=createPinia()
store.use(piniaPluginPersist)
createApp(App).use(store).mount(
'#app')

在对应的store中开启,数据默认存在 sessionStorage 里,并且会以 storeId 作为 key

import { defineStore } from 'pinia'
//'main' 是storeId
export const useMainStore = defineStore('main', {
state: ()
=>({
counter:
2,
name:
'Eduardo',
isAdmin:
true}),//…… //开启数据缓存 persist: {
enabled:
true}
})

如果需要自定义key和存储位置,则修改参数即可。

persist: {
enabled:
true,
strategies: [
//使用插件自定义存储 {
key:
'settings', //key可以自己定义,不填的话默认就是这个store的ID storage: localStorage,
}
]
},

4、在实际项目中使用pinia

一般项目开发,实际上存储的内容会比较多,可能根据不同的键值模块进行区分,因此把它们放在一个store/modules里面,方便的使用引用它来存取设置数据即可。

我们这里简单以一个settings的配置信息进行介绍,其中index.ts是一个统一的创建pinia的对象并挂接到全局App上的。

其中index.ts的代码如下所示。

import type { App } from "vue";
import { createPinia } from
"pinia";
import piniaPluginPersist from
'pinia-plugin-persist';//使用插件持久化 const store=createPinia();
store.use(piniaPluginPersist)
//使用插件持久化 exportfunction setupStore(app: App<Element>) {
app.use(store);
}

export { store };

因此在main.js里面引入并挂接pinia即可。

import { createApp } from 'vue'import ElementPlus from'element-plus'import'element-plus/dist/index.css'import'normalize.css' //css初始化
import App from'./App.vue'import { setupStore } from "/@/store";

const app
=createApp(App)
setupStore(app)
app.use(ElementPlus)
app.mount(
'#app')

这样我们就可以再次定义一个模块化的配置信息,以便于管理存储各种不同类型的内容。

如下面我们定义一个程序配置信息setttings.ts

import { defineStore } from "pinia";
import { store } from
"/@/store";

export type settingType
={
title: string;
fixedHeader:
boolean;
hiddenSideBar:
boolean;
};

export const useSettingStore
=defineStore({
id:
"settings",
state: (): settingType
=>({
title:
"Vue3 + TypeScript + Element",
fixedHeader:
false,
hiddenSideBar:
false}),
persist: {
enabled:
true,
strategies: [
//使用插件自定义存储 {
key:
'settings', //key可以自己定义,不填的话默认就是这个store的ID storage: localStorage,
}
]
},

getters: {
getTitle() {
return this.title;
},
getFixedHeader() {
return this.fixedHeader;
},
getHiddenSideBar() {
return this.HiddenSideBar;
}
},
actions: {
CHANGE_SETTING({ key, value }) {
//eslint-disable-next-line no-prototype-builtins if (this.hasOwnProperty(key)) {this[key] =value;
}
},
changeSetting(data) {
this.CHANGE_SETTING(data);
}
}
});

export
functionuseSettingStoreHook() {returnuseSettingStore(store);
}

然后在组件视图vue或者app.vue中使用即可

<scriptlang="ts">import { defineComponent } from"vue";

import { useSettingStoreHook } from
"/@/store/modules/settings";
import { storeToRefs } from
"pinia";

export
defaultdefineComponent({
name:
"app",
components: {
},
setup() {
const store
=useSettingStoreHook();

const { fixedHeader, title }
=storeToRefs(store);return{
fixedHeader,
title,
};
},
methods: {
setTitle() {
this.title= "Vue3 + TypeScript + Element + Edit";
console.log(
this.title);
},
},
});
</script>

查看数据修改后,存储在本地存储空间中的内容,如下所示。

由于我们有时候需要在基于.net framework的项目上使用(如Winform端应用),有时候有需要在.net core的项目上使用(如.net core的WebAPI),那么我们把基于SQLSugar的基础模块封装,编译为.net standard就很有必要,而且由于.net framework和.net core在配置文件上的差异,我们需要对基础配置信息进行不同环境的兼容处理,以便实现基础模块支持.net FrameWork和.net core的项目调用。

1、基于.netStandard的类库模块

在上篇随笔《
基于SqlSugar的数据库访问处理的封装,支持多数据库并使之适应于实际业务开发中
(1)》中介绍了对SqlSugar 基础模块的封装处理,为了兼容不同类型的框架,我们可以把它们封装为.net Standard类库。

但是 为了基础模块能够顺利加载不同.net框架下的配置文件信息来初始化数据库连接,我们需要配置不同的加载处理方式来读取处理。

如.net Framework的使用App.config中读取配置信息,而.net core项目中使用读取 appSettings.json里面的配置信息。

因此需要让它们兼容,我们需要修改项目文件中的TargetFrameworks,让它根据不同的框架生成不同的DLL类库,从而达到支持不同环境下配置文件的读取处理。

这样我们查看项目属性,就可以看到类库是支持多种目标框架的了。

接下来我们对DbContext辅助类进行改动,让它根据不同的目标框架条件来读取配置信息。

统一入口就是调用ConfigHelper辅助类来隔离处理操作。

        publicDbContext()
{
this.DbSetting =ConfigHelper.GetDbSettings();
Init(
this.DbSetting.DbConfigName);
}
public DbContext(stringdbConfigName)
{
if (this.DbSetting == null)
{
this.DbSetting =ConfigHelper.GetDbSettings();
}
Init(dbConfigName);
}

以替代原先的处理代码。原先的只能从.net framework的App.config中读取,现在我们需要根据框架来判断处理。

        publicDbContext()
{
//默认采用配置项名//appSettings/DefaultDb 配置项为指定连接字符串的name var dbConfigName = ConfigurationManager.AppSettings["DefaultDb"];
Init(dbConfigName);
}

我们先来定义一个承载数据库信息的实体类对象。

    /// <summary>
    ///数据库配置信息/// </summary>
    public classDbSetting
{
/// <summary> ///默认指定的连接字符串集合的配置项名称/// </summary> public string DbConfigName { get; set; }/// <summary> ///数据库类型,默认为SQLServer/// </summary> public string DbType { get; set; } = "sqlserver";/// <summary> ///连接字符串/// </summary> public string ConnectionString { get; set; }

}

在目标框架为.net framwork的时候,我们的App.Config配置信息是下面的。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <!--Sqlserver数据库的连接字符串-->
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI" />
    <!--MySQL数据库的连接字符串-->
    <addname="mysql"providerName="MySql.Data.MySqlClient"connectionString="Server=localhost;Database=winframework;Uid=root;Pwd=123456;SslMode=none" />
    <!--sqlite数据库字符串,路径符号|DataDirectory|代表当前运行目录-->
    <addname="sqlite"providerName="System.Data.SQLite"connectionString="Data Source=|DataDirectory|\WinFramework.db;Version=3;" />
    <!--PostgreSQL数据库的连接字符串-->
    <addname="npgsql"providerName="Npgsql"connectionString="Server=localhost;Port=5432;Database=winframework;User Id=postgres;Password=123456" />
    <!--不受驱动影响,32位64位均可使用-->
    <addname="oracle"providerName="OracleManaged"connectionString="Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=win;Password=win" />
    <!--达梦数据库的连接字符串-->
    <addname="Dm"providerName="Dm"connectionString="Server=localhost;User ID=SYSDBA;PWD=SYSDBA;Database=WINFRAMEWORK;" />
  </connectionStrings>
  
  <appSettings>
    <!--指定默认的数据库类型,如果不指定则使用第一个连接字符串-->
    <addkey="DefaultDb"value="sqlserver" />
    <!--字典、权限组件的数据库类型:mysql、npgsql、oracle、sqlite、sqlserver等,默认为sqlserver可不写-->
    <addkey="ComponentDbType"value="sqlserver" />
  </appSettings>
  <startup>
    <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.8" />
  </startup>

而如果是基于.net core的情况下,读取的是appSettings.json里面的配置信息,配置文件信息如下所示。

{"ConnectionStrings": {"Default": "Server=.; Database=WeixinBootstrap2; Trusted_Connection=True;","Oracle": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=C##ABP;Password=abp","MySql": "Server=localhost;Database=myprojectdb;Uid=root;Pwd=123456;","PostgreSQL": "Server=localhost;Port=5432;Database=myprojectdb;User Id=postgres;Password=123456"},"DbSetting": {"DefaultDb": "Default","ComponentDbType": "sqlserver"},

...........

}

其中 DefaultDb 指向的是默认的连接字符串配置节点名称,而ComponentDbType为它的数据库类型,如sqlserver,mysql这样的标识。

2、根据不同目标框架读取配置信息

有了上面不同目标框架下的配置信息节点的格式,我们就可以根据目标框架的不同来相应读取配置信息,从而实现不同的初始化处理操作。

        /// <summary>
        /// 根据承载环境是netframework或者是.netcore,读取配置信息。
        /// 在.netframework中的app.config的appSettings中配置DefaultDb,ComponentDbType,连接字符串在connectionStrings中配置
        /// 在.netcore中的appSettings.json的DbSetting节点中配置DefaultDb,ComponentDbType,连接字符串在ConnectionStrings中配置
        /// </summary>
        /// <returns></returns>
public static DbSetting GetDbSettings()
{
//初始化承载配置信息对象 var dbSetting = newDbSetting();

#
ifNETFRAMEWORK//基于.net frameowork下读取app.config的配置 dbSetting.DbType = ConfigurationManager.AppSettings["ComponentDbType"] ?? "sqlserver";
dbSetting.DbConfigName
= ConfigurationManager.AppSettings["DefaultDb"];var setting = ConfigurationManager.ConnectionStrings[1];//默认第一个连接字符串 if (!string.IsNullOrWhiteSpace(dbSetting.DbConfigName))
{
//如果配置节点名称存在,则读取它的连接字符串 setting =ConfigurationManager.ConnectionStrings[dbSetting.DbConfigName];
}
if (setting != null)
{
dbSetting.ConnectionString
=setting.ConnectionString;
}
#
else //基于.net core下的读取appsettings.json的配置信息 dbSetting.DbType = GetSectionValue("DbSetting:ComponentDbType") ?? "sqlserver";
dbSetting.DbConfigName
= GetSectionValue("DbSetting:DefaultDb");var connectionStringKey = string.Format("ConnectionStrings:{0}", dbSetting.DbConfigName);
dbSetting.ConnectionString
=GetSectionValue(connectionStringKey);
#endif
returndbSetting;
}

我们可以从类的顶部来选择对应的分类,从而实现代码的加亮显示,便于代码的编写。

另外,我们根据配置信息的数据库类型,通过遍历判断的方式来转换为SqlSugar对应的数据库类型即可。

这样DbContext初始化的时候,就能够顺利适用于不同的目标框架中了,我们在SQLSugar封装的基类就可以不管它的具体处理,只需要初始化DbContext即可,如下代码所示。

基类调用来处理常规的对象返回操作,代码如下所示。

        /// <summary>
        /// 获取所有记录
        /// </summary>
        public virtual async Task<ListResultDto<TEntity>>GetAllAsync()
{
var list =await EntityDb.GetListAsync();return new ListResultDto<TEntity>()
{
Items
=list
};
}

而如果我们需要联合多表来实现联合查询,也可以使用基类的对象进行处理。

如对于字典来说,根据字典大类名称来获取字典项目信息,而字典项目表里面,只有字典大类的ID,那么就需要联合字典大类和字典项目两个表进行关联查询了,如下代码所示。

        /// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeName">字典类型名称</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByDictType(stringdictTypeName)
{
var query = this.dbContent.Client.Queryable<DictDataInfo, DictTypeInfo>(
(d, t)
=> d.DictType_ID == t.Id && t.Name ==dictTypeName)
.Select(d
=> d); //联合条件获取对象 query= query.OrderBy(d => d.DictType_ID).OrderBy(d => d.Seq);//排序 var list = await query.ToListAsync();//获取列表 var dict = new Dictionary<string, string>();foreach (var info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Value);
}
}
returndict;
}

这样就可以实现联合表的查询处理。

至此,我们就可以无差别的在不同的目标框架上,根据不同的配置文件来初始化我们的DbContext类,从而无差别的使用基于SqlSugar的数据库访问处理的基类,简化了框架的处理。

相关随笔列表如下所示。

基于SqlSugar的数据库访问处理的封装,支持.net FrameWork和.net core的项目调用
(本篇随笔)

我前面几篇随笔介绍了关于几篇关于SqlSugar的基础封装,已经可以直接应用在Winform项目开发上,并且基础接口也通过了单元测试,同时测试通过了一些Winform功能页面;本篇随笔继续深化应用开发,着手在在.net6框架的Web API上开发应用,也就是基于.net core的Web API应用开发,这样可以应用在不同的前端接入上。本篇随笔主要介绍基于.net6框架的Web API的相关整合开发内容,内容涉及到Swagger的整合支持、SeriLog的支持、JWT鉴权和用户身份信息缓存、基类控制器封装、自动注入接口对象、统一结果封装、统一异常处理等方面。

1、创建.netcore WebApi项目并添加相关支持

本篇随笔主要从基础框架开发创建,因此使用VS2022添加一个基于.net core6的WebAPI项目,如下所示。

我们在生成的项目中,看到有一个Program.cs的代码文件,里面代码比较简洁,我们逐步调整并添加自己的相关代码即可。

在其中可以看到

builder.Services.AddSwaggerGen();

这个是简单的Swagger注释支持,我们如果需要定义更多的信息,可以采用下面的代码。

#region 添加swagger注释

//builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(c=>{
c.SwaggerDoc(
"v1", newOpenApiInfo
{
Version
= "v1",
Title
= "Api"});
c.IncludeXmlComments(Path.Combine(basePath,
"SugarWebApi.xml"), true);//WebAPI项目XML文件 c.IncludeXmlComments(Path.Combine(basePath, "SugarProjectCore.xml"), true);//其他项目所需的XML文件 c.AddSecurityDefinition("Bearer", newOpenApiSecurityScheme
{
Description
= "Value: Bearer {token}",
Name
= "Authorization",
In
=ParameterLocation.Header,
Type
=SecuritySchemeType.ApiKey,
Scheme
= "Bearer"});
c.AddSecurityRequirement(
newOpenApiSecurityRequirement()
{
{
newOpenApiSecurityScheme
{
Reference
= newOpenApiReference
{
Type
=ReferenceType.SecurityScheme,
Id
= "Bearer"},Scheme= "oauth2",Name = "Bearer",In =ParameterLocation.Header,
},
new List<string>()
}
});
});
#endregion

上面的代码除了添加对应控制器的接口信息外,还增加了一个相关服务类的接口定义,便于我们查看详细的xml信息,如下所示得到很详细的接口注释。

然后调整Swagger UI支持的代码如下所示。

//Configure the HTTP request pipeline.
if(app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

另外,我们的Web API控制器,需要集成JWT Bear 认证的处理的,添加认证代码如下所示。

//JWT Bear 认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{
options.TokenValidationParameters
= newTokenValidationParameters
{
//非固定可选可加 ValidateIssuer = true,
ValidIssuer
= builder.Configuration["Jwt:Issuer"],
ValidateAudience
= true,
ValidAudience
= builder.Configuration["Jwt:Audience"],

ValidateLifetime
= true,//时间 ClockSkew =TimeSpan.Zero,
IssuerSigningKey
= new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]))
};
});

这里面的代码读取配置信息的,我们可以在appSettings.json中配置JWT的一些键值。

  "Jwt": {"Secret": "your-256-bit-secret","Issuer": "iqidi.com","Audience": "api"}

另外,为了在.net core中输出日志,可以使用SeriLog组件进行处理。

添加相关的nuget类库,如下所示。

然后在Program.cs中添加初始化日志代码即可。

//初始化日志
Log.Logger = newLoggerConfiguration()
.MinimumLevel.Debug()
//最小记录级别//对其他日志进行重写,除此之外,目前框架只有微软自带的日志组件 .MinimumLevel.Override(source: "Microsoft", minimumLevel: Serilog.Events.LogEventLevel.Error)

.Enrich.FromLogContext()
//记录相关上下文信息 .WriteTo.Console() //输出到控制台 .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day) //输出到本地文件 .CreateLogger();

2、统一结果封装和异常处理

在Web API的控制器返回信息中,我们为了方便使用JSON信息,往往需要对返回结果进行封装,让它们返回指定格式的数据,如下所示。

正常结果200:

未授权结果401:

关于接口数据格式的统一封装,我们定义一个WrapResultFilter,以及需要一个不封装的属性标识DontWrapResultAttribute,默认是统一封装返回的结果。

    /// <summary>
    ///禁用封装结果/// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]public classDontWrapResultAttribute : Attribute
{
}

而统一封装的处理,需要继承ActionFilterAttribute并重写OnResultExecuting处理操作。

里面的主要逻辑就是对结果内容进行统一的再次封装,如下所示主要的逻辑代码。

if (context.Result isObjectResult objRst)
{
if (objRst.Value isAjaxResponse)return;

context.Result
= new ObjectResult(newAjaxResponse
{
Success
= true,
Error
= null,
TargetUrl
= string.Empty,
UnAuthorizedRequest
= false,
Result
=objRst.Value
});
}

除了常规的正常返回内容进行封装,也需要对异常进行拦截,并对结果进行封装,因此需要继承ExceptionFilterAttribute并添加一个异常处理的过滤器进行处理,并重写OnException的操作即可。

    /// <summary>
    ///自定义异常处理/// </summary>
    public classGlobalExceptionFilter : ExceptionFilterAttribute
{
/// <summary> ///异常处理封装/// </summary> /// <param name="context"></param> public override voidOnException(ExceptionContext context)
{
if (!context.ExceptionHandled)
{
//异常返回结果包装 var content = newAjaxResponse()
{
Success
= false,
Error
= new ErrorInfo(0, context.Exception.Message, context.Exception.StackTrace),
TargetUrl
= string.Empty,
UnAuthorizedRequest
= false,
Result
= null};//日志记录 Log.Error(context.Exception, context.Exception.Message);

context.ExceptionHandled
= true;
context.Result
= newApplicationErrorResult(content);
context.HttpContext.Response.StatusCode
= (int)HttpStatusCode.InternalServerError;
}
}

因此为了拦截相关的处理,我们在Program.cs中添加以下代码进行拦截。

//控制器添加自定义过滤器
builder.Services.AddControllers(options=>{
options.Filters.Add
<WrapResultFilter>(); //统一结果封装处理 options.Filters.Add<GlobalExceptionFilter>();//自定义异常处理 });//所有控制器启动身份验证 builder.Services.AddMvc(options =>{
options.Filters.Add(
new AuthorizeFilter());//所有MVC服务默认添加授权标签 });

并调整代码,添加认证和授权验证的代码处理。

app.UseAuthentication();

app.UseAuthorization();

对于一些系统异常的处理(如401未授权、400未找到接口、500系统错误)等,默认是没有进行处理的

我们如果要拦截,就另外需要添加一个中间件的方式来处理信息流,如下所示。

其中在Invoke的函数处理中,统一处理不同的异常即可。

public asyncTask Invoke(HttpContext context)
{
try{awaitnext(context);
}
catch(Exception ex)
{
var statusCode =context.Response.StatusCode;if (ex isArgumentException)
{
statusCode
= 200;
}
awaitHandleExceptionAsync(context, statusCode, ex.Message);
}
finally{var statusCode =context.Response.StatusCode;var msg = "";if (statusCode == 401)
{
msg
= "未授权" + context.Response.Headers["WWW-Authenticate"];

}
else if (statusCode == 404)
{
msg
= "未找到服务";
}
else if(statusCode == 500)
{
msg
= "系统错误";
}
else if (statusCode == 502)
{
msg
= "请求错误";
}
else if (statusCode != 200)
{
msg
= "未知错误";
}
if (!string.IsNullOrWhiteSpace(msg))
{
awaitHandleExceptionAsync(context, statusCode, msg);
}
}
}

并添加一个扩展类方法,用于快速使用中间件方式调用。

    /// <summary>
    ///自定义错误处理的扩展方法/// </summary>
    public static classErrorHandlingExtensions
{
public static IApplicationBuilder UseErrorHandling(thisIApplicationBuilder builder)
{
return builder.UseMiddleware<ErrorHandlingMiddleware>();
}
}

最后在program.cs代码中添加使用代码即可,注意添加位置。

另外,为了把用户身份信息缓存起来,我们可以使用Redis进行缓存处理,因此在项目中使用CRedis的封装类库进行操作Redis

通过连接字符串(读取配置信息)初始化Redis的代码如下所示。

//初始化Redis
RedisHelper.Initialization(new CSRedisClient(builder.Configuration["CSRedis:ConnectString"]));

其中appSettings.json信息如下所示。

{
"ConnectionStrings": {
"Default": "Server=.; Database=WeixinBootstrap2; Trusted_Connection=True;",
"Oracle": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=C##ABP;Password=abp",
"MySql": "Server=localhost;Database=myprojectdb;Uid=root;Pwd=123456;",
"PostgreSQL": "Server=localhost;Port=5432;Database=myprojectdb;User Id=postgres;Password=123456"
},
"DbSetting": {
"DefaultDb": "Default",
"ComponentDbType": "sqlserver"
},
"CSRedis": {
"ConnectString": "127.0.0.1:6379"
},
"Jwt": {
"Secret": "your-256-bit-secret",
"Issuer": "iqidi.com",
"Audience": "api"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
}

如果允许登录授权请求成功的话,那么对应的用户省份缓存也就记录在Redis中了。

3、接口对象的依赖注入处理

我们在.net core的Web API中调用相关处理,我们往往是使用接口来调用相关的处理的。

启动Web API的时候通过手工或者自动注入接口对象的方式,然后在控制器里面的构造函数中通过依赖注入方式使用接口即可。

如果是手工注入,那么你确定在Web API项目中所有用到的业务访问接口,都需要提取注入。

    services.AddSingleton<IDictDataService, DictDataService>();
services.AddSingleton
<IDictTypeService, DictTypeService>();
services.AddSingleton
<ICustomerService, CustomerService>();
services.AddScoped
<IUserService, UserService>();

但是这样接口实现一旦很多,手工加入肯定繁琐而且低效了,因此需要考虑自动注入所有相关的服务接口为佳。

为了实现这个自动注入的目标,首先我们先定义几个不同生命周期的接口声明。

    //用于定义这三种生命周期的标识接口

    public interfaceIDependency
{
}
/// <summary> ///瞬时(每次都重新实例)/// </summary> public interfaceITransientDependency : IDependency
{
}
/// <summary> ///单例(全局唯一)/// </summary> public interfaceISingletonDependency : IDependency
{

}
/// <summary> ///一个请求内唯一(线程内唯一)/// </summary> public interfaceIScopedDependency : IDependency
{
}

然后通过遍历相关的DLL,然后实现所有实现指定接口的类对象,统一加入即可,如下代码所示。

            var baseType = typeof(IDependency);var path = AppDomain.CurrentDomain.RelativeSearchPath ??AppDomain.CurrentDomain.BaseDirectory;var getFiles = Directory.GetFiles(path, "*.dll").Where(Match);  //.Where(o=>o.Match())
            var referencedAssemblies = getFiles.Select(Assembly.LoadFrom).ToList();  //.Select(o=> Assembly.LoadFrom(o))

            var ss = referencedAssemblies.SelectMany(o => o.GetTypes());

然后进一步进行对接口的判断,如下所示。

    var types =referencedAssemblies
.SelectMany(a
=>a.DefinedTypes)
.Select(type
=>type.AsType())
.Where(x
=> x != baseType &&baseType.IsAssignableFrom(x)).ToList();var implementTypes = types.Where(x =>x.IsClass).ToList();var interfaceTypes = types.Where(x =>x.IsInterface).ToList();foreach (var implementType inimplementTypes)
{
if (typeof(IScopedDependency).IsAssignableFrom(implementType))
{
var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddScoped(interfaceType, implementType);
}
else if (typeof(ISingletonDependency).IsAssignableFrom(implementType))
{
var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddSingleton(interfaceType, implementType);
}
else{var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddTransient(interfaceType, implementType);
}
}

然后统一调用即可。

//配置依赖注入访问数据库
ServiceInjection.ConfigureRepository(builder.Services);

这样我们在对应的WebAPI 控制器中就可以方便的使用接口的构造函数注入方式了。

    /// <summary>
    ///客户信息的控制器对象/// </summary>
    public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto>{privateICustomerService _customerService;/// <summary>
        ///构造函数,并注入基础接口对象/// </summary>
        /// <param name="customerService"></param>
        public CustomerController(ICustomerService customerService) :base(customerService)
{
this._customerService =customerService;
}
}

以上就是我们在创建.net Core项目的Web API项目中碰到的一些常见问题的总结,希望对大家有所帮助。

相关系类文章如下所示。

基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用
(本随笔)

在实际项目开发中,我们可能会碰到各种各样的项目环境,有些项目需要一个大而全的整体框架来支撑开发,有些中小项目这需要一些简单便捷的系统框架灵活开发。目前大型一点的框架,可以采用ABP或者ABP VNext的框架,两者整体思路和基础设计类似,不过ABP侧重于一个独立完整的项目框架,开发的时候统一整合处理;而ABP VNext则是以微服务架构为基础,各个模块独立开发,既可以整合在一个项目中,也可以以微服务进行单独发布,并统一通过网关处理进行交流。不管ABP或者ABP VNext框架,都集合了.NET CORE领域众多技术为一体,并且基础类设计上,错综复杂,关系较多,因此开发学习有一定的门槛,中小型项目应用起来有一定的费劲之处。本系列随笔介绍底层利用SqlSugar来做ORM数据访问模块,设计一个简单便捷一点的框架,本篇从基础开始介绍一些框架内容,参照一些ABP/ABP VNext中的一些类库处理,来承载类似条件分页信息,查询条件处理等处理细节。

1、基于SqlSugar开发框架的架构设计

主要的设计模块场景如下所示。

为了避免像ABP VNext框架那样分散几十个项目,我们尽可能聚合内容放在一个项目里面。

1)其中一些常用的类库,以及SqlSugar框架的基类放在框架公用模块里面。

2)Winform开发相关的基础界面以及通用组件内容,放在基础Winform界面库BaseUIDx项目中。

3)基础核心数据模块SugarProjectCore,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。其中Service里面一些框架基类和接口定义,统一也放在公用类库里面。

4)Winform应用模块,主要就是针对业务开发的WInform界面应用,而WInform开发为了方便,也会将一些基础组件和基类放在了BaseUIDx的Winform专用的界面库里面。

5)WebAPI项目采用基于.net Core6的项目开发,通过调用SugarProjectCore实现相关控制器API的发布,并整合Swagger发布接口,供其他前端界面应用进行调用。

6)纯前端通过API进行调用Web API的接口,纯前端模块可以包含Vue3&Element项目,以及基于EelectronJS应用,发布跨平台的基于浏览器的应用界面,以及其他App或者小程序整合Web API进行业务数据的处理或者展示需要。

如后端开发,我们可以在VS2022中进行管理,管理开发Winform项目、Web API项目等。

Winform界面,我们可以采用基于.net Framework开发或者.net core6进行开发均可,因为我们的SugarProjectCore项目是采用.net Standard模式开发,兼容两者。这里以权限模块来进行演示整合使用。

而纯前端的项目,我们可以基于VSCode或者 HBuilderX等工具进行项目的管理开发工作。

2、框架基础类的定义和处理

在开发一个易于使用的框架的时候,主要目的就是减少代码开发,并尽可能通过基类和泛型约束的方式,提高接口的通用性,并通过结合代码生成工具的方式,来提高标准项目的开发效率。

那么我们这里基于SqlSugar的ORM处理,来实现常规数据的增删改查等常规操作的时候,我们是如何进行这些接口的封装处理的呢。

例如,我们对于一个简单的客户信息表,如下所示。

那么它生成的SqlSugar实体类如下所示。

    /// <summary>
    ///客户信息///继承自Entity,拥有Id主键属性/// </summary>
    [SugarTable("T_Customer")]public class CustomerInfo : Entity<string>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicCustomerInfo()
{
this.CreateTime =System.DateTime.Now;
}
#region Property Members /// <summary> ///姓名/// </summary> public virtual string Name { get; set; }/// <summary> ///年龄/// </summary> public virtual int Age { get; set; }/// <summary> ///创建人/// </summary> public virtual string Creator { get; set; }/// <summary> ///创建时间/// </summary> public virtual DateTime CreateTime { get; set; }#endregion}

其中 Entity<string> 是我们根据需要定义一个基类实体对象,主要就是定义一个Id的属性来处理,毕竟对于一般表对象的处理,SqlSugar需要Id的主键定义(非中间表处理)。

[Serializable]public abstract class Entity<TPrimaryKey> : IEntity<TPrimaryKey>{/// <summary>
        ///实体类唯一主键/// </summary>
        [SqlSugar.SugarColumn(IsPrimaryKey = true, ColumnDescription = "主键")]public virtual TPrimaryKey Id { get; set; }
}

而IEntity<T>定义了一个接口

    public interface IEntity<TPrimaryKey>{/// <summary>
        ///实体类唯一主键/// </summary>
        TPrimaryKey Id { get; set; }
}

以上就是实体类的处理,我们一般为了查询信息,往往通过一些条件传入进行处理,那么我们就需要定义一个通用的分页查询对象,供我们精准进行条件的处理。

生成一个以***PageDto的对象类,如下所示。

    /// <summary>
    ///用于根据条件分页查询,DTO对象/// </summary>
    public classCustomerPagedDto : PagedAndSortedInputDto, IPagedAndSortedResultRequest
{
/// <summary> ///默认构造函数/// </summary> public CustomerPagedDto() : base() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skipCount">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public CustomerPagedDto(int skipCount, int resultCount) : base(skipCount, resultCount)
{
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> public CustomerPagedDto(PagerInfo pagerInfo) : base(pagerInfo)
{
}
#region Property Members /// <summary> ///不包含的对象的ID,用于在查询的时候排除对应记录/// </summary> public virtual string ExcludeId { get; set; }/// <summary> ///姓名/// </summary> public virtual string Name { get; set; }/// <summary> ///年龄-开始/// </summary> public virtual int? AgeStart { get; set; }/// <summary> ///年龄-结束/// </summary> public virtual int? AgeEnd { get; set; }/// <summary> ///创建时间-开始/// </summary> public DateTime? CreateTimeStart { get; set; }/// <summary> ///创建时间-结束/// </summary> public DateTime? CreateTimeEnd { get; set; }#endregion}

其中PagedAndSortedInputDto, IPagedAndSortedResultRequest都是参考来自于ABP/ABP VNext的处理方式,这样我们可以便于数据访问基类的查询处理操作。

接着我们定义一个基类MyCrudService,并传递如相关的泛型约束,如下所示

    /// <summary>
    ///基于SqlSugar的数据库访问操作的基类对象/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public abstract class MyCrudService<TEntity, TKey, TGetListInput>: 
IMyCrudService
<TEntity, TKey, TGetListInput> where TEntity : class, IEntity<TKey>, new()where TGetListInput : IPagedAndSortedResultRequest

我们先忽略基类接口的相关实现细节,我们看看对于这个
MyCrudService

IMyCrudService
我们应该如何使用的。

首先我们定义一个应用层的接口ICustomerService如下所示。

    /// <summary>
    ///客户信息服务接口/// </summary>
    public interface ICustomerService : IMyCrudService<CustomerInfo, string, CustomerPagedDto>, ITransientDependency
{

}

然后实现在CustomerService中实现它的接口。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService

这样我们对于特定Customer的接口在ICustomer中定义,标准接口直接调用基类即可。

基类MyCrudService提供重要的两个接口,让子类进行重写,以便于进行准确的条件处理和排序处理,如下代码所示。

    /// <summary>
    ///基于SqlSugar的数据库访问操作的基类对象/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public abstract class MyCrudService<TEntity, TKey, TGetListInput>: 
IMyCrudService
<TEntity, TKey, TGetListInput> where TEntity : class, IEntity<TKey>, new()whereTGetListInput : IPagedAndSortedResultRequest
{
/// <summary> ///留给子类实现过滤条件的处理/// </summary> /// <returns></returns> protected virtual ISugarQueryable<TEntity>CreateFilteredQueryAsync(TGetListInput input)
{
returnEntityDb.AsQueryable();
}
/// <summary> ///默认排序,通过ID进行排序/// </summary> /// <param name="query"></param> /// <returns></returns> protected virtual ISugarQueryable<TEntity> ApplyDefaultSorting(ISugarQueryable<TEntity>query)
{
if (typeof(TEntity).IsAssignableTo<IEntity<TKey>>())
{
return query.OrderBy(e =>e.Id);
}
else{return query.OrderBy("Id");
}
}
}

对于Customer特定的业务对象来说,我们需要实现具体的条件查询细节和排序条件,毕竟我们父类没有约束确定实体类有哪些属性的情况下,这些就交给子类做最合适了。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
/// <summary> ///自定义条件处理/// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<CustomerInfo>CreateFilteredQueryAsync(CustomerPagedDto input)
{
var query = base.CreateFilteredQueryAsync(input);

query
=query
.WhereIF(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIF(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals//年龄区间查询 .WhereIF(input.AgeStart.HasValue, s => s.Age >=input.AgeStart.Value)
.WhereIF(input.AgeEnd.HasValue, s
=> s.Age <=input.AgeEnd.Value)//创建日期区间查询 .WhereIF(input.CreateTimeStart.HasValue, s => s.CreateTime >=input.CreateTimeStart.Value)
.WhereIF(input.CreateTimeEnd.HasValue, s
=> s.CreateTime <=input.CreateTimeEnd.Value)
;
returnquery;
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <returns></returns> protected override ISugarQueryable<CustomerInfo> ApplyDefaultSorting(ISugarQueryable<CustomerInfo>query)
{
return query.OrderBy(t =>t.CreateTime, OrderByType.Desc);//先按第一个字段排序,然后再按第二字段排序//return base.ApplySorting(query, input).OrderBy(s=>s.Customer_ID).OrderBy(s => s.Seq); }
}

通过 CreateFilteredQueryAsync 的精确条件处理,我们就可以明确实体类的查询条件处理,因此对于CustomerPagedDto来说,就是可以有客户端传入,服务后端的基类进行处理了。

如基类的分页条件查询函数GetListAsync就是根据这个来处理的,它的实现代码如下所示。

        /// <summary>
        ///根据条件获取列表/// </summary>
        /// <param name="input">分页查询条件</param>
        /// <returns></returns>
        public virtual async Task<PagedResultDto<TEntity>>GetListAsync(TGetListInput input)
{
var query =CreateFilteredQueryAsync(input);var totalCount = awaitquery.CountAsync();

query
=ApplySorting(query, input);
query
=ApplyPaging(query, input);var list = awaitquery.ToListAsync();return new PagedResultDto<TEntity>(
totalCount,
list
);
}

而其中 ApplySorting 就是根据条件决定是否选择子类实现的默认排序进行处理的。

        /// <summary>
        ///记录排序处理/// </summary>
        /// <returns></returns>
        protected virtual ISugarQueryable<TEntity> ApplySorting(ISugarQueryable<TEntity>query, TGetListInput input)
{
//Try to sort query if available if (input isISortedResultRequest sortInput)
{
if (!sortInput.Sorting.IsNullOrWhiteSpace())
{
returnquery.OrderBy(sortInput.Sorting);
}
}
//IQueryable.Task requires sorting, so we should sort if Take will be used. if (input isILimitedResultRequest)
{
returnApplyDefaultSorting(query);
}
//No sorting returnquery;
}

对于获取单一对象,我们一般提供一个ID主键获取即可。

        /// <summary>
        ///根据ID获取单一对象/// </summary>
        /// <param name="id">主键ID</param>
        /// <returns></returns>
        public virtual async Task<TEntity>GetAsync(TKey id)
{
return awaitEntityDb.GetByIdAsync(id);
}

也可以根据用户的Express条件进行处理,在基类我们定义很多这样的Express条件处理,便于子类进行条件处理的调用。如对于删除,可以指定ID,也可以指定条件删除。

        /// <summary>
        ///删除指定ID的对象/// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool>DeleteAsync(TKey id)
{
return awaitEntityDb.DeleteByIdAsync(id);
}
/// <summary>
        ///根据指定条件,删除集合/// </summary>
        /// <param name="input">表达式条件</param>
        /// <returns></returns>
        public virtual async Task<bool> DeleteAsync(Expression<Func<TEntity, bool>>input)
{
var result = awaitEntityDb.DeleteAsync(input);returnresult;
}

如判断是否存在也是一样处理

        /// <summary>
        ///判断是否存在指定条件的记录/// </summary>
        /// <param name="id">ID 主键</param>
        /// <returns></returns>
        public virtual async Task<bool>IsExistAsync(TKey id)
{
var info = awaitEntityDb.GetByIdAsync(id);var result = (info != null);returnresult;
}
/// <summary> ///判断是否存在指定条件的记录/// </summary> /// <param name="input">表达式条件</param> /// <returns></returns> public virtual async Task<bool> IsExistAsync(Expression<Func<TEntity, bool>>input)
{
var result = awaitEntityDb.IsAnyAsync(input);returnresult;
}

关于Web API的处理,我在随笔《
基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用
》中也有介绍,主要就是先弄好.net6的开发环境,然后在进行相关的项目开发即可。

根据项目的需要,我们定义了一些控制器的基类,用于实现不同的功能。

其中ControllerBase是.net core Web API中的标准控制器基类,我们由此派生一个LoginController用于登录授权,而BaseApiController则处理常规接口用户身份信息,而BusinessController则是对标准的增删改查等基础接口进行的封装,我们实际开发的时候,只需要开发编写类似CustomerController基类即可。

BaseApiController没有什么好介绍的,就是封装一下获取用户的身份信息。

可以通过下面代码获取接口用户的Id

        /// <summary>
        ///当前用户身份ID/// </summary>
        protected virtual string? CurrentUserId => HttpContext.User.FindFirst(JwtClaimTypes.Id)?.Value;

而BusinessController控制器则是继承这个BaseApiController即可。通过泛型约束传入相关的对象信息。

    /// <summary>
    ///本控制器基类专门为访问数据业务对象而设的基类/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    [Route("[controller]")]
[Authorize]
//需要授权登录访问 public class BusinessController<TEntity, TKey, TGetListInput> : BaseApiController where TEntity : class, IEntity<TKey>, new()whereTGetListInput : IPagedAndSortedResultRequest
{
/// <summary> ///通用基础操作接口/// </summary> protected IMyCrudService<TEntity, TKey, TGetListInput> _service { get; set; }/// <summary> ///构造函数,初始化基础接口/// </summary> /// <param name="service">通用基础操作接口</param> public BusinessController(IMyCrudService<TEntity, TKey, TGetListInput>service)
{
this._service =service;
}

....

这个基类接收一个符合基类接口定义的对象作为基类增删删改查等处理方法的接口对象。在具体的CustomerController中的定义处理如下所示。

    /// <summary>
    ///客户信息的控制器对象/// </summary>
    public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto>
{privateICustomerService _customerService;/// <summary>
        ///构造函数,并注入基础接口对象/// </summary>
        /// <param name="customerService"></param>
        public CustomerController(ICustomerService customerService) :base(customerService)
{
this._customerService =customerService;
}
}

这样就可以实现基础的相关操作了。如果需要特殊的接口实现,那么定义方法实现即可。

类似字典项目中的控制器处理代码如下所示。定义好HTTP方法,路由信息等即可。

        /// <summary>
        ///根据字典类型ID获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
[HttpGet]
[Route(
"by-typeid/{dictTypeId}")]public async Task<Dictionary<string, string>> GetDictByTypeID(stringdictTypeId)
{
return await_dictDataService.GetDictByTypeID(dictTypeId);
}
/// <summary> ///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary> /// <param name="dictTypeName">字典类型名称</param> /// <returns></returns> [HttpGet]
[Route(
"by-typename/{dictTypeName}")]public async Task<Dictionary<string, string>> GetDictByDictType(stringdictTypeName)
{
return await_dictDataService.GetDictByDictType(dictTypeName);
}

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在前面介绍的SqlSugar的相关查询处理操作中,我们主要以单表的方式生成相关的实体类,并在查询的时候,对单表的字段进行条件的对比处理,从而返回对应的数据记录。本篇随笔介绍在一些外键或者中间表的处理中,如何遍历查询并获得所需的记录操作。

1、回顾单表的操作查询

我在《
基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用
》中介绍过的Customer表信息,就是一个单表的处理。

例如,我们对于一个简单的客户信息表,如下所示。

生成对应的实体对象CustomerInfo外,同时生成 CustomerPagedDto  的分页查询条件对象。

在继承基类后

/// <summary>
///应用层服务接口实现/// </summary>
public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
....
}

并重写 CreateFilteredQueryAsync 函数,从而实现了条件的精确查询处理。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
/// <summary> ///自定义条件处理/// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<CustomerInfo>CreateFilteredQueryAsync(CustomerPagedDto input)
{
var query = base.CreateFilteredQueryAsync(input);

query
=query
.WhereIF(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIF(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals//年龄区间查询 .WhereIF(input.AgeStart.HasValue, s => s.Age >=input.AgeStart.Value)
.WhereIF(input.AgeEnd.HasValue, s
=> s.Age <=input.AgeEnd.Value)//创建日期区间查询 .WhereIF(input.CreateTimeStart.HasValue, s => s.CreateTime >=input.CreateTimeStart.Value)
.WhereIF(input.CreateTimeEnd.HasValue, s
=> s.CreateTime <=input.CreateTimeEnd.Value)
;
returnquery;
}

在表的对应实体信息没有其他表关联的时候,我们直接通过SqlSugar的基础接口返回对象列表即可。

通过 CreateFilteredQueryAsync 的精确条件处理,我们就可以明确实体类的查询条件处理,因此对于CustomerPagedDto来说,就是可以有客户端传入,服务后端的基类进行处理了。

如基类的分页条件查询函数GetListAsync就是根据这个来处理的,它的实现代码如下所示。

        /// <summary>
        ///根据条件获取列表/// </summary>
        /// <param name="input">分页查询条件</param>
        /// <returns></returns>
        public virtual async Task<PagedResultDto<TEntity>>GetListAsync(TGetListInput input)
{
var query =CreateFilteredQueryAsync(input);var totalCount = awaitquery.CountAsync();

query
=ApplySorting(query, input);
query
=ApplyPaging(query, input);var list = awaitquery.ToListAsync();return new PagedResultDto<TEntity>(
totalCount,
list
);
}

也就是说只要继承了 CustomerService ,我们默认调用基类的 GetListAsync 就可以返回对应的列表记录了。

如在Web API的控制器中调用获取记录返回,调用处理的代码如下所示。

        /// <summary>
        ///获取所有记录/// </summary>
[HttpGet]
[Route(
"all")]
[HttpGet]
public virtual async Task<ListResultDto<TEntity>>GetAllAsync()
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey);return await_service.GetAllAsync();
}

而对于Winform的调用,我们这里首先利用代码生成工具生成对应的界面和代码

查看其调用的界面代码

而其中GetData中的函数部分内容如下所示。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<CustomerInfo>>GetData()
{
CustomerPagedDto pagerDto
= null;if (advanceCondition != null)
{
//如果有高级查询,那么根据输入信息构建查询条件 pagerDto = new CustomerPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
=dlg.GetPagedResult(pagerDto);
}
else{//构建分页的条件和查询条件 pagerDto = new CustomerPagedDto(this.winGridViewPager1.PagerInfo)
{
//添加所需条件 Name = this.txtName.Text.Trim(),
};
//日期和数值范围定义//年龄,需在CustomerPagedDto中添加 int? 类型字段AgeStart和AgeEnd var Age = new ValueRange<int?>(this.txtAge1.Text, this.txtAge2.Text); //数值类型 pagerDto.AgeStart =Age.Start;
pagerDto.AgeEnd
=Age.End;//创建时间,需在CustomerPagedDto中添加 DateTime? 类型字段CreationTimeStart和CreationTimeEnd var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型 pagerDto.CreateTimeStart =CreationTime.Start;
pagerDto.CreateTimeEnd
=CreationTime.End;
}
var result = await BLLFactory<CustomerService>.Instance.GetListAsync(pagerDto);returnresult;
}

列表界面效果如下所示。

2、基于中间表的查询处理

前面的查询处理,主要就是针对没有任何关系的表实体对象的返回处理,但往往我们开发的时候,会涉及到很多相关的表,单独的表相对来说还是比较少,因此对表的关系遍历处理和中间表的关系转换,就需要在数据操作的时候考虑的了。

例如对于字典大类和字典项目的关系,如下所示。

以及在权限管理系统模块中,用户、角色、机构、权限等存在着很多中间表的关系,如下所示。

如对于字典表关系处理,我们采用Queryable<DictDataInfo, DictTypeInfo>的查询处理方式,可以联合两个表对象实体进行联合查询,如下代码所示。

        /// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeName">字典类型名称</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByDictType(stringdictTypeName)
{
var query = this.Client.Queryable<DictDataInfo, DictTypeInfo>(
(d, t)
=> d.DictType_ID == t.Id && t.Name ==dictTypeName)
.Select(d
=> d); //联合条件获取对象 query= query.OrderBy(d => d.DictType_ID).OrderBy(d => d.Seq);//排序 var list = await query.ToListAsync();//获取列表 var dict = new Dictionary<string, string>();foreach (var info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Value);
}
}
returndict;
}

其中的Client对象是DbContext对象实例的Client属性,如下图所示。

这个对象是在DbContext对象中构建的,如下所示。

            this.Client = new SqlSugarScope(newConnectionConfig()
{
DbType
= this.DbType,
ConnectionString
= this.ConnectionString,
InitKeyType
=InitKeyType.Attribute,
IsAutoCloseConnection
= true, //是否自动关闭连接 AopEvents = newAopEvents
{
OnLogExecuting
= (sql, p) =>{//Log.Information(sql);//Log.Information(string.Join(",", p?.Select(it => it.ParameterName + ":" + it.Value))); }
}
});

我们查看Queryable,可以看到这个SqlSugar基类函数 Queryable 提供了很多重载函数,也就是它们可以提供更多的表对象进行联合查询的,如下所示。

前面介绍的是外键的一对多的关系查询,通过两个对象之间进行的关系连接,从而实现另一个对象属性的对比查询操作的。

对于中间表的处理,也是类似的情况,我们通过对比中间表的属性,从而实现条件的过滤处理。如下是对于角色中相关关系的中间表查询。

        /// <summary>
        ///根据用户ID获取对应的角色列表/// </summary>
        /// <param name="userID">用户ID</param>
        /// <returns></returns>
        private async Task<List<RoleInfo>> GetByUser(intuserID)
{
var query = this.Client.Queryable<RoleInfo, User_RoleInfo>(
(t, m)
=> t.Id == m.Role_ID && m.User_ID ==userID)
.Select(t
=> t); //联合条件获取对象 query= query.OrderBy(t => t.CreateTime);//排序 var list = await query.ToListAsync();//获取列表 returnlist;
}
/// <summary> ///根据机构获取对应的角色列表(判断机构角色中间表)/// </summary> /// <param name="ouID">机构的ID</param> /// <returns></returns> public async Task<List<RoleInfo>> GetRolesByOu(intouID)
{
var query = this.Client.Queryable<RoleInfo, OU_RoleInfo>(
(t, m)
=> t.Id == m.Role_ID && m.Ou_ID ==ouID)
.Select(t
=> t); //联合条件获取对象 query= query.OrderBy(t => t.CreateTime);//排序 var list = await query.ToListAsync();//获取列表 returnlist;
}

通过联合查询中间表对象信息,可以对它的字段属性进行条件联合,从而获得所需的记录。

这里User_RoleInfo和Ou_RoleInfo表也是根据中间表的属性生成的,不过它们在业务层并没有任何关联操作,也不需要生成对应的Service层,因此只需要生成相关的Model类实体即可。

    /// <summary>
    ///用户角色关联/// </summary>
    [SugarTable("T_ACL_User_Role")]public classUser_RoleInfo
{
/// <summary> ///用户ID/// </summary> [Required]public virtual int User_ID { get; set; }/// <summary> ///角色ID/// </summary> [Required]public virtual int Role_ID { get; set; }

}
    /// <summary>
    ///机构角色关联/// </summary>
    [SugarTable("T_ACL_OU_Role")]public classOU_RoleInfo 
{
/// <summary> ///机构ID/// </summary> [Required]public virtual int Ou_ID { get; set; }/// <summary> ///角色ID/// </summary> [Required]public virtual int Role_ID { get; set; }
}

可以看到这两个实体不同于其他实体,它们没有基类继承关系,而一般标准的实体是有的。

    /// <summary>
    ///角色信息/// </summary>
    [SugarTable("T_ACL_Role")]public class RoleInfo : Entity<int> {  }


    /// <summary>
    ///功能菜单/// </summary>
    [SugarTable("T_ACL_Menu")]public class MenuInfo : Entity<string> { }

所以我们就不需要构建它们的Service层来处理数据,它的存在合理性只是在于能够和其他实体对象进行表的联合查询处理而且。

最后贴上一个整合SqlSugar处理而完成的系统基础框架的Winform端界面,其中包括用户、组织机构、角色管理、权限管理、菜单管理、日志、字典、客户信息等业务表的处理。

以证所言非虚。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面