在基于SqlSugar的开发框架中,我们设计了一些系统服务层的基类,在基类中会有很多涉及到相关的数据处理操作的,如果需要跟踪具体是那个用户进行操作的,那么就需要获得当前用户的身份信息,包括在Web API的控制器中也是一样,需要获得对应的用户身份信息,才能进行相关的身份鉴别和处理操作。本篇随笔介绍基于Principal的用户身份信息的存储和读取操作,以及在适用于Winform程序中的内存缓存的处理方式,从而通过在基类接口中注入用户身份信息接口方式,获得当前用户的详细身份信息。

1、用户身份接口的定义和基类接口注入

为了方便获取用户身份的信息,我们定义一个接口 IApiUserSession 如下所示。

   /// <summary>
/// API接口授权获取的用户身份信息-接口
/// </summary>
public interface IApiUserSession
{
/// <summary>
/// 用户登录来源渠道,0为网站,1为微信,2为安卓APP,3为苹果APP
/// </summary>
string Channel { get; } /// <summary>
/// 用户ID
/// </summary>
int? Id { get; } /// <summary>
/// 用户名称
/// </summary>
string Name { get; } /// <summary>
/// 用户邮箱(可选)
/// </summary>
string Email { get; } /// <summary>
/// 用户手机(可选)
/// </summary>
string Mobile { get; } /// <summary>
/// 用户全名称(可选)
/// </summary>
string FullName { get; } /// <summary>
/// 性别(可选)
/// </summary>
string Gender { get; } /// <summary>
/// 所属公司ID(可选)
/// </summary>
string Company_ID { get; } /// <summary>
/// 所属公司名称(可选)
/// </summary>
string CompanyName { get; } /// <summary>
/// 所属部门ID(可选)
/// </summary>
string Dept_ID { get; } /// <summary>
/// 所属部门名称(可选)
/// </summary>
string DeptName { get; } /// <summary>
/// 把用户信息设置到缓存中去
/// </summary>
/// <param name="info">用户登陆信息</param>
/// <param name="channel">默认为空,用户登录来源渠道:0为网站,1为微信,2为安卓APP,3为苹果APP </param>
void SetInfo(LoginUserInfo info, string channel = null);
}

其中的SetInfo是为了在用户身份登录确认后,便于将用户信息存储起来的一个接口方法。其他属性定义用户相关的信息。

由于这个用户身份信息的接口,我们提供给基类进行使用的,默认我们在基类定义一个接口对象,并通过提供默认的NullApiUserSession实现,便于引用对应的身份属性信息。

NullApiUserSession只是提供一个默认的实现,实际在使用的时候,我们会注入一个具体的接口实现来替代它的。

    /// <summary>
/// 提供一个空白实现类,具体使用IApiUserSession的时候,会使用其他实现类
/// </summary>
public class NullApiUserSession : IApiUserSession
{
/// <summary>
/// 单件实例
/// </summary>
public static NullApiUserSession Instance { get; } = new NullApiUserSession(); public string Channel => null; public int? Id => null; public string Name => null;

................../// <summary>
/// 设置信息(保留为空)
/// </summary>
public void SetInfo(LoginUserInfo info, string channel = null)
{
}
}

在之前介绍的SqlSugar框架的时候,我们介绍到数据访问操作的基类定义,如下所示。

    /// <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
{
/// <summary>
/// 数据库上下文信息
/// </summary>
protected DbContext dbContext;/// <summary>
/// 当前Api用户信息
/// </summary>
public IApiUserSession CurrentApiUser { get; set; } public MyCrudService()
{
dbContext = new DbContext(); CurrentApiUser = NullApiUserSession.Instance;//空实现
}

在最底层的操作基类中,我们就已经注入了用户身份信息,这样我们不管操作任何函数处理,都可以通过该用户身份信息接口CurrentApiUser获得对应的用户属性信息了。

在具体的业务服务层中,我们继承该基类,并提供构造函数注入方式,让基类获得对应的 IApiUserSession接口的具体实例。

    /// <summary>
/// 应用层服务接口实现
/// </summary>
public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="currentApiUser">当前用户接口</param>
public CustomerService(IApiUserSession currentApiUser)
{
this.CurrentApiUser = currentApiUser;
} ........ }

如果有其他服务接口需要引入,那么我们继续增加其他接口注入即可。

    /// <summary>
/// 角色信息 应用层服务接口实现
/// </summary>
public class RoleService : MyCrudService<RoleInfo,int, RolePagedDto>, IRoleService
{
private IOuService _ouService;
private IUserService _userService; /// <summary>
/// 默认构造函数
/// </summary>
/// <param name="currentApiUser">当前用户接口</param>
/// <param name="ouService">机构服务接口</param>
/// <param name="userService">用户服务接口</param>
public RoleService(IApiUserSession currentApiUser, IOuService ouService, IUserService userService)
{
this.CurrentApiUser = currentApiUser;
this._ouService = ouService;
this._userService = userService;
}

由于该接口是通过构造函数注入的,因此在系统运行前,我们需要往IOC容器中注册对应的接口实现类(由于IApiUserSession 提供了多个接口实现,我们这里不自动加入它的对应接口,而通过手工加入)。

在Winform或者控制台程序,启动程序的时候,手工加入对应的接口到IOC容器中即可。

/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
// IServiceCollection负责注册
IServiceCollection services = new ServiceCollection();
//services.AddSingleton<IDictDataService, DictDataService>(); //调用自定义的服务注册
ServiceInjection.ConfigureRepository(services); //添加IApiUserSession实现类
//services.AddSingleton<IApiUserSession, ApiUserCache>(); //缓存实现方式
services.AddSingleton<IApiUserSession, ApiUserPrincipal>(); //CurrentPrincipal实现方式

如果是Web API或者asp.net core项目中加入,也是类似的处理方式。

var builder = WebApplication.CreateBuilder(args);
//配置依赖注入访问数据库
ServiceInjection.ConfigureRepository(builder.Services); //添加IApiUserSession实现类
builder.Services.AddSingleton<IApiUserSession, ApiUserPrincipal>();

前面介绍了,IApiUserSession的一个空白实现,是默认的接口实现,我们具体会使用基于Principal或者缓存方式实现记录用户身份的信息实现,如下是它们的类关系。

在上面的代码中,我们注入一个 ApiUserPrincipal 的用户身份接口实现。

2、基于Principal的用户身份信息的存储和读取操作

ApiUserPrincipal 的用户身份接口实现是可以实现Web及Winform的用户身份信息的存储的。

首先我们先定义一些存储声明信息的键,便于统一处理。

    /// <summary>
/// 定义一些常用的ClaimType存储键
/// </summary>
public class ApiUserClaimTypes
{
public const string Id = JwtClaimTypes.Id;
public const string Name = JwtClaimTypes.Name;
public const string NickName = JwtClaimTypes.NickName;
public const string Email = JwtClaimTypes.Email;
public const string PhoneNumber = JwtClaimTypes.PhoneNumber;
public const string Gender = JwtClaimTypes.Gender;
public const string FullName = "FullName";
public const string Company_ID = "Company_ID";
public const string CompanyName = "CompanyName";
public const string Dept_ID = "Dept_ID";
public const string DeptName = "DeptName"; public const string Role = ClaimTypes.Role;
}

ApiUserPrincipal 用户身份接口实现的定义如下代码所示。

    /// <summary>
/// 基于ClaimsPrincipal实现的用户信息接口。
/// </summary>
[Serializable]
public class ApiUserPrincipal : IApiUserSession
{
/// <summary>
/// IHttpContextAccessor对象
/// </summary>
private readonly IHttpContextAccessor _httpContextAccessor; /// <summary>
/// 如果IHttpContextAccessor.HttpContext?.User非空获取HttpContext的ClaimsPrincipal,否则获取线程的CurrentPrincipal
/// </summary>
protected ClaimsPrincipal Principal => _httpContextAccessor?.HttpContext?.User ?? (Thread.CurrentPrincipal as ClaimsPrincipal); /// <summary>
/// 默认构造函数
/// </summary>
/// <param name="httpContextAccessor"></param>
public ApiUserPrincipal(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
} /// <summary>
/// 默认构造函数
/// </summary>
public ApiUserPrincipal() { }

基于Web  API的时候,用户身份信息是基于IHttpContextAccessor 注入的接口获得 httpContextAccessor?.HttpContext?.User 的 ClaimsPrincipal 属性操作的。

我们获取用户身份的属性的时候,直接通过这个属性判断获取即可。

        /// <summary>
/// 用户ID
/// </summary>
public int? Id => this.Principal?.FindFirst(ApiUserClaimTypes.Id)?.Value.ToInt32(); /// <summary>
/// 用户名称
/// </summary>
public string Name => this.Principal?.FindFirst(ApiUserClaimTypes.Name)?.Value;

而上面同时也提供了一个基于Windows的线程Principal 属性(Thread.CurrentPrincipal )的声明操作,操作模型和Web 的一样的,因此Web和WinForm的操作是一样的。

在用户登录接口处理的时候,我们需要统一设置一下用户对应的声明信息,存储起来供查询使用。

        /// <summary>
/// 主要用于Winform写入Principal的ClaimsIdentity
/// </summary>
public void SetInfo(LoginUserInfo info, string channel = null)
{
//new WindowsPrincipal(WindowsIdentity.GetCurrent()); var claimIdentity = new ClaimsIdentity("login");
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Id, info.ID ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Name, info.UserName ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Email, info.Email ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.PhoneNumber, info.MobilePhone ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Gender, info.Gender ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.FullName, info.FullName ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Company_ID, info.CompanyId ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.CompanyName, info.CompanyName ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Dept_ID, info.DeptId ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.DeptName, info.DeptName ?? "")); //此处不可以使用下面注释代码
//this.Principal?.AddIdentity(claimIdentity); //Thread.CurrentPrincipal设置会导致在异步线程中设置的结果丢失
//因此统一采用 AppDomain.CurrentDomain.SetThreadPrincipal中设置,确保进程中所有线程都会复制到信息
IPrincipal principal = new GenericPrincipal(claimIdentity, null);
AppDomain.CurrentDomain.SetThreadPrincipal(principal);
}

在上面中,我特别声明“Thread.CurrentPrincipal设置会导致在异步线程中设置的结果丢失” ,这是我在反复测试中发现,不能在异步方法中设置Thread.CurrentPrincipal的属性,否则属性会丢失,因此主线程的Thread.CurrentPrincipal 会赋值替换掉异步线程中的Thread.CurrentPrincipal属性。

而.net 提供了一个程序域的方式设置CurrentPrincipal的方法,可以或者各个线程中统一的信息。

AppDomain.CurrentDomain.SetThreadPrincipal(principal);

基于WInform的程序,我们在登录界面中处理用户登录操作

但用户确认登录的时候,测试用户的账号密码,成功则在本地设置用户的身份信息。

        /// <summary>
/// 统一设置登陆用户相关的信息
/// </summary>
/// <param name="info">当前用户信息</param>
public async Task SetLoginInfo(LoginResult loginResult)
{
var info = loginResult.UserInfo; //用户信息 //获取用户的角色集合
var roles = await BLLFactory<IRoleService>.Instance.GetRolesByUser(info.Id);
//判断用户是否超级管理员||公司管理员
var isAdmin = roles.Any(r => r.Name == RoleInfo.SuperAdminName || r.Name == RoleInfo.CompanyAdminName); //初始化权限用户信息
Portal.gc.UserInfo = info; //登陆用户
Portal.gc.RoleList = roles;//用户的角色集合
Portal.gc.IsUserAdmin = isAdmin;//是否超级管理员或公司管理员
Portal.gc.LoginUserInfo = this.ConvertToLoginUser(info); //转换为窗体可以缓存的对象 //设置身份信息到共享对象中(Principal或者Cache)
BLLFactory<IApiUserSession>.Instance.SetInfo(Portal.gc.LoginUserInfo); await Task.CompletedTask;
}

通过SetInfo,我们把当前用户的信息设置到了域的Principal中,进程内的所有线程共享这份用户信息数据。

跟踪接口的调用,我们可以查看到对应的用户身份信息了。

可以看到,这个接口已经注入到了服务类中,并且获得了相应的用户身份信息了。

同样在Web API的登录处理的时候,会生成相关的JWT token的信息的。

           var loginResult = await this._userService.VerifyUser(dto.LoginName, dto.Password, ip);
if (loginResult != null && loginResult.UserInfo != null)
{
var userInfo = loginResult.UserInfo; authResult.AccessToken = GenerateToken(userInfo); //令牌
authResult.Expires = expiredDays * 24 * 3600; //失效秒数
authResult.Succes = true;//成功 //设置缓存用户信息
//SetUserCache(userInfo);
}
else
{
authResult.Error = loginResult?.ErrorMessage;
}

其中生成的JWT token的逻辑如下所示。

        /// <summary>
/// 生成JWT用户令牌
/// </summary>
/// <returns></returns>
private string GenerateToken(UserInfo userInfo)
{
var claims = new List<Claim>
{
new Claim(ApiUserClaimTypes.Id, userInfo.Id.ToString()),
new Claim(ApiUserClaimTypes.Email, userInfo.Email),
new Claim(ApiUserClaimTypes.Name, userInfo.Name),
new Claim(ApiUserClaimTypes.NickName, userInfo.Nickname),
new Claim(ApiUserClaimTypes.PhoneNumber, userInfo.MobilePhone),
new Claim(ApiUserClaimTypes.Gender, userInfo.Gender), new Claim(ApiUserClaimTypes.FullName, userInfo.FullName),
new Claim(ApiUserClaimTypes.Company_ID, userInfo.Company_ID),
new Claim(ApiUserClaimTypes.CompanyName, userInfo.CompanyName),
new Claim(ApiUserClaimTypes.Dept_ID, userInfo.Dept_ID),
new Claim(ApiUserClaimTypes.DeptName, userInfo.DeptName),
}; var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]));
var jwt = new JwtSecurityToken
(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddDays(expiredDays),//有效时间
signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
return token;
}

说生成的一系列字符串,我们可以通过解码工具,可以解析出来对应的信息的。

在登录授权的这个时候,控制器会把相关的Claim信息写入到token中的,我们在客户端发起对控制器方法的调用的时候,这些身份信息会转换成对象信息。

我们调试控制器的方法入口,可以看到CurrentApiUser的信息就是我们发起用户身份信息,如下图所示。

在监视窗口中查看IApiUserSession对象,可以查看到对应的信息。

3、基于内存缓存的用户身份接口实现处理方式

在前面介绍的IApiUserSession的接口实现的时候,我们也提供了另外一个基于MemoryCache的缓存实现方式,和基于Principal凭证信息处理不同,我们这个是基于MemoryCache的存储方式。

它的实现方法也是类似的,我们这里也一并介绍一下。

    /// <summary>
/// 基于MemeoryCache实现的用户信息接口
/// </summary>
public class ApiUserCache : IApiUserSession
{
/// <summary>
/// 内存缓存对象
/// </summary>
private static readonly ObjectCache Cache = MemoryCache.Default; /// <summary>
/// 默认构造函数
/// </summary>
public ApiUserCache()
{
} /// <summary>
/// 把用户信息设置到缓存中去
/// </summary>
/// <param name="info">用户登陆信息</param>
public void SetInfo(LoginUserInfo info, string channel = null)
{
SetItem(ApiUserClaimTypes.Id, info.ID);
SetItem(ApiUserClaimTypes.Name, info.UserName);
SetItem(ApiUserClaimTypes.Email, info.Email);
SetItem(ApiUserClaimTypes.PhoneNumber, info.MobilePhone);
SetItem(ApiUserClaimTypes.Gender, info.Gender);
SetItem(ApiUserClaimTypes.FullName, info.FullName);
SetItem(ApiUserClaimTypes.Company_ID, info.CompanyId);
SetItem(ApiUserClaimTypes.CompanyName, info.CompanyName);
SetItem(ApiUserClaimTypes.Dept_ID, info.DeptId);
SetItem(ApiUserClaimTypes.DeptName, info.DeptName);
} /// <summary>
/// 设置某个属性对象
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
private void SetItem(string key, object value)
{
if (!string.IsNullOrEmpty(key))
{
Cache.Set(key, value ?? "", DateTimeOffset.MaxValue, null);
}
} /// <summary>
/// 用户ID
/// </summary>
public int? Id => (Cache.Get(ApiUserClaimTypes.Id) as string)?.ToInt32(); /// <summary>
/// 用户名称
/// </summary>
public string Name => Cache.Get(ApiUserClaimTypes.Name) as string; /// <summary>
/// 用户邮箱(可选)
/// </summary>
public string Email => Cache.Get(ApiUserClaimTypes.Email) as string; ..............
}

我们通过 MemoryCache.Default 构造一个内存缓存的对象,然后在设置信息的时候,把用户信息按照键值方式设置即可。在Winform中我们可以采用内存缓存的方式存储用户身份信息,而基于Web方式的,则会存在并发多个用户的情况,不能用缓存来处理。

一般情况下,我们采用 ApiUserPrincipal 来处理用户身份信息就很好了。

4、单元测试的用户身份处理

在做单元测试的时候,我们如果需要设置测试接口的用户身份信息,那么就需要在初始化函数里面设置好用户信息,如下所示。

    [TestClass]
public class UnitTest1
{
private static IServiceProvider Provider = null; /*
带有[ClassInitialize()] 特性的方法在执行类中第一个测试之前调用。
带有[TestInitialize()] 特性的方法在执行每个测试前都会被调用,一般用来初始化环境,为单元测试配置一个特定已知的状态。
带有[ClassCleanup()] 特性的方法将在类中所有的测试运行完后执行。
*/
//[TestInitialize] //每个测试前调用
[ClassInitialize] //测试类第一次调用
public static void Setup(TestContext context)
{
// IServiceCollection负责注册
IServiceCollection services = new ServiceCollection();
//调用自定义的服务注册
ServiceInjection.ConfigureRepository(services); //注入当前Api用户信息处理实现,服务对象可以通过IApiUserSession获得用户信息
//services.AddSingleton<IApiUserSession, ApiUserCache>(); //缓存实现方式
services.AddSingleton<IApiUserSession, ApiUserPrincipal>(); //CurrentPrincipal实现方式 // IServiceProvider负责提供实例
Provider = services.BuildServiceProvider(); //模拟写入登录用户信息
WriteLoginInfo();
} /// <summary>
/// 写入用户登陆信息,IApiUserSession接口才可使用获取身份
/// </summary>
static void WriteLoginInfo()
{
var mockUserInfo = new LoginUserInfo()
{
ID = "1",
Email = "wuhuacong@163.com",
MobilePhone = "18620292076",
UserName = "admin",
FullName = "伍华聪"
}; //通过使用全局IServiceProvider的接口获得服务接口实例
Provider.GetService<IApiUserSession>().SetInfo(mockUserInfo);
}

上面的方法初始化了测试类的信息,方法调用的时候,我们获得对应的接口实例处理即可,如下测试代码所示。

        /// <summary>
/// 测试查找记录
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task TestMethod1()
{
var input = new DictTypePagedDto()
{
Name = "客户"
}; var service = Provider.GetService<IDictTypeService>();
var count = await service.CountAsync(s => true);
Assert.AreNotEqual(0, count); var list = await service.GetAllAsync();
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count > 0); list = await service.GetListAsync(input);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count > 0); var ids = list.Items.Select(s => { return s.Id; }).Take(2);
list = await service.GetAllByIdsAsync(ids);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count > 0); var id = list.Items[0].Id;
var info = await service.GetAsync(id);
Assert.IsNotNull(info);
Assert.AreEqual(id, info.Id); info = await service.GetFirstAsync(s => true);
Assert.IsNotNull(info); await Task.CompletedTask;
}

系列文章:

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

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

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

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

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

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

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

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

    我喜欢在一个项目开发模式成熟的时候,使用代码生成工具Database2Sharp来配套相关的代码生成,对于我介绍的基于SqlSugar的开发框架,从整体架构确定下来后,我就着手为它们量身定做相关的代码 ...

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

    我们在设计数据库表的时候,往往为了方便,主键ID一般采用字符串类型或者GUID类型,这样对于数据库表记录的迁移非常方便,而且有时候可以在处理关联记录的时候,提前对应的ID值.但有时候进行数据记录插入的 ...

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

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

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

    在实际项目开发中,我们可能会碰到各种各样的项目环境,有些项目需要一个大而全的整体框架来支撑开发,有些中小项目这需要一些简单便捷的系统框架灵活开发.目前大型一点的框架,可以采用ABP或者ABP VNex ...

  5. 这里给大家介绍一下通过 Wi-Fi 连接“慷慨捐赠”你的身份信息的七种方法.

    这里给大家介绍一下通过 Wi-Fi 连接“慷慨捐赠”你的身份信息的七种方法和反制措施. 本文作者:黑子小盆友 1.利用免费热点 它们似乎无处不在,而且它们的数量会在接下来四年里增加三倍.但是它们当中很 ...

  6. 基于MVC4+EasyUI的Web开发框架形成之旅(6)--基类控制器CRUD的操作

    在上一篇随笔中,我对Web开发框架的总体界面进行了介绍,其中并提到了我的<Web开发框架>的控制器的设计关系,Web开发框架沿用了我的<Winform开发框架>的很多架构设计思 ...

  7. 基于ASP.NET MVC的热插拔模块式开发框架(OrchardNoCMS)介绍(二)

    基于ASP.NET MVC的热插拔模块式开发框架(OrchardNoCMS)介绍(二) 之前文章中给大家说明了下我这个小小的想法,发现还是有不少人的支持和关注.你们的鼓励是对我最大的支持. 我总结了了 ...

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

    我前面几篇随笔介绍了关于几篇关于SqlSugar的基础封装,已经可以直接应用在Winform项目开发上,并且基础接口也通过了单元测试,同时测试通过了一些Winform功能页面:本篇随笔继续深化应用开发 ...

  9. 线上分享-- 基于DDD的.NET开发框架-ABP介绍

    前言 为了能够帮助.Net开发者开拓视野,更好的把最新的技术应用到工作中,我在3月底受邀到如鹏网.net训练营直播间为各位学弟学妹们进行ABP框架的直播分享.同时为了让更多的.NET开发者了解ABP框 ...

  10. VUE+Element 前端应用开发框架功能介绍

    前面介绍了很多ABP系列的文章<ABP框架使用>,一步一步的把我们日常开发中涉及到的Web API服务构建.登录日志和操作审计日志.字典管理模块.省份城市的信息维护.权限管理模块中的组织机 ...

随机推荐

  1. nginx小记

    上一次折腾nginx还是两年前的事情了.好多配置都忘记了. 捣腾了下阿里云,部署了一下,遇到几个小问题,温故并记录一下吧 :) 重新设置 nginx遇到问题:nginx: [error] invali ...

  2. HashSet源码详解

    序言 在写了HashMap文章后,隔了几天才继续这一系列的文章,因为要学的东西实在是太多了,写一篇要花费的时间很多,所以导致隔了几天才来写.不过希望自己坚持下去.终有一天会拨开云雾见青天的.学Hash ...

  3. 基于Laravel+Swoole开发智能家居后端

    基于Laravel+Swoole开发智能家居后端 在上一篇<Laravel如何优雅的使用Swoole>中我已经大概谈到了Laravel结合Swoole的用法. 今天,我参与的智能家居项目基 ...

  4. linux中编译git时提示找不到ssl.h头文件

    在centos中的解决方案是安装一个叫 openssl-devel 的包.

  5. Android拍照、录像、录音代码范例

    <p>import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import andro ...

  6. 国庆第三天2014年10月3日10:21:39,Nutz,WebCollector,jsoup

    (1)做得好,做得快,只能选择一样. (2)时间过得很快,你没法在假期的一天里完成更多的计划.假期全部由自己支配,相对长一点的睡眠,新加入的娱乐(视频或者游戏),你不比在工作中更有效率. (3)每天练 ...

  7. 安卓高级1 -----Xutil3 和Picasso使用

    Xutils3 Xutils由于内部使用httpclient然而在安卓5.0谷歌发现httpclient出现不稳定的情况.于6.0完全弃用,所以作者升级到Xutils3替换原本网络模块 配置环境(St ...

  8. go语言打造个人博客系统(二)

    go语言打造个人博客系统(二)   在上篇文章go语言打造个人博客系统(一)中,我们了解了go语言的优点和go语言的数据库操作,本次我们会完成博客系统的后端开发. 博客系统后端接口开发 路由测试 ht ...

  9. [转] mongoose学习笔记(超详细)

    名词解释 Schema: 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力 Model: 由Schema编译而成的假想(fancy)构造器,具有抽象属性和行为.Model的每一个实例(ins ...

  10. shell中使用&gt;/dev/null 2&gt;&amp;1 丢弃信息

    在一些Shell脚本中,特别是Crontab的脚本中,经常会看到 >/dev/null 2>&1这样的写法. 其实这个很好理解.我们分两部分解释. 1.  >/dev/nul ...