mirror of
https://github.com/anjoy8/Blog.Core.git
synced 2024-09-20 23:48:27 +08:00
33
This commit is contained in:
parent
e2f52b054f
commit
810bf5979a
|
@ -47,6 +47,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Blog.Core.Extensions\Blog.Core.Extensions.csproj" />
|
||||
<ProjectReference Include="..\Blog.Core.SqlSugarDbRepository\Blog.Core.SqlSugarDbRepository.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -503,7 +503,7 @@
|
|||
用户管理
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Blog.Core.Controllers.UserController.#ctor(Blog.Core.IRepository.UnitOfWork.IUnitOfWork,Blog.Core.IServices.ISysUserInfoServices,Blog.Core.IServices.IUserRoleServices,Blog.Core.IServices.IRoleServices,Blog.Core.Common.HttpContextUser.IUser,Microsoft.Extensions.Logging.ILogger{Blog.Core.Controllers.UserController})">
|
||||
<member name="M:Blog.Core.Controllers.UserController.#ctor(Blog.Core.IRepository.UnitOfWork.IUnitOfWork,Blog.Core.IServices.ISysUserInfoServices,Blog.Core.IServices.IUserRoleServices,Blog.Core.IServices.IRoleServices,Blog.Core.Common.HttpContextUser.IUser,Blog.Core.SqlSugarDbRepository.Interface.ISqlSugarProviderStorage{Blog.Core.SqlSugarDbRepository.Interface.ISqlSugarProvider},Blog.Core.SqlSugarDbRepository.Interface.ISqlSugarRepository{Blog.Core.Model.IDS4DbModels.ApplicationUser},Microsoft.Extensions.Logging.ILogger{Blog.Core.Controllers.UserController})">
|
||||
<summary>
|
||||
构造函数
|
||||
</summary>
|
||||
|
@ -512,6 +512,8 @@
|
|||
<param name="userRoleServices"></param>
|
||||
<param name="roleServices"></param>
|
||||
<param name="user"></param>
|
||||
<param name="sqlSugarProviderStorage"></param>
|
||||
<param name="sqlRepository"></param>
|
||||
<param name="logger"></param>
|
||||
</member>
|
||||
<member name="M:Blog.Core.Controllers.UserController.Get(System.Int32,System.String)">
|
||||
|
|
|
@ -8,10 +8,14 @@ using Blog.Core.Common.HttpContextUser;
|
|||
using Blog.Core.IRepository.UnitOfWork;
|
||||
using Blog.Core.IServices;
|
||||
using Blog.Core.Model;
|
||||
using Blog.Core.Model.IDS4DbModels;
|
||||
using Blog.Core.Model.Models;
|
||||
using Blog.Core.SqlSugarDbRepository;
|
||||
using Blog.Core.SqlSugarDbRepository.Interface;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Blog.Core.Controllers
|
||||
{
|
||||
|
@ -28,6 +32,8 @@ namespace Blog.Core.Controllers
|
|||
readonly IUserRoleServices _userRoleServices;
|
||||
readonly IRoleServices _roleServices;
|
||||
private readonly IUser _user;
|
||||
private readonly ISqlSugarProviderStorage<ISqlSugarProvider> _sqlSugarProviderStorage;
|
||||
private readonly ISqlSugarRepository<ApplicationUser> _sqlRepository;
|
||||
private readonly ILogger<UserController> _logger;
|
||||
|
||||
/// <summary>
|
||||
|
@ -38,14 +44,22 @@ namespace Blog.Core.Controllers
|
|||
/// <param name="userRoleServices"></param>
|
||||
/// <param name="roleServices"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="sqlSugarProviderStorage"></param>
|
||||
/// <param name="sqlRepository"></param>
|
||||
/// <param name="logger"></param>
|
||||
public UserController(IUnitOfWork unitOfWork, ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, IRoleServices roleServices, IUser user, ILogger<UserController> logger)
|
||||
public UserController(IUnitOfWork unitOfWork, ISysUserInfoServices sysUserInfoServices, IUserRoleServices userRoleServices, IRoleServices roleServices
|
||||
, IUser user
|
||||
, ISqlSugarProviderStorage<ISqlSugarProvider> sqlSugarProviderStorage
|
||||
, ISqlSugarRepository<ApplicationUser> sqlRepository
|
||||
, ILogger<UserController> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_sysUserInfoServices = sysUserInfoServices;
|
||||
_userRoleServices = userRoleServices;
|
||||
_roleServices = roleServices;
|
||||
_user = user;
|
||||
_sqlSugarProviderStorage = sqlSugarProviderStorage;
|
||||
_sqlRepository = sqlRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
@ -105,6 +119,48 @@ namespace Blog.Core.Controllers
|
|||
return "value";
|
||||
}
|
||||
|
||||
// GET: api/User/5
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public object GetIds4User()
|
||||
{
|
||||
var result = new List<ApplicationUser>() { };
|
||||
var DbName = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=BlogIdpPro;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";
|
||||
_sqlSugarProviderStorage.AddOrUpdate(DbName, new SqlSugarDbRepository.SqlSugarProvider(new SqlSugarSetting()
|
||||
{
|
||||
Name = DbName,
|
||||
ConnectionString = DbName,
|
||||
DatabaseType = DbType.SqlServer,
|
||||
LogExecuting = (sql, pars) =>
|
||||
{
|
||||
Console.WriteLine($"sql:{sql}");
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
using (_sqlRepository.ChangeProvider(DbName))
|
||||
{
|
||||
result = _sqlRepository.GetCurrentSqlSugar().Queryable<ApplicationUser>()
|
||||
.Where(s => s.age >= 0)
|
||||
.ToList();
|
||||
|
||||
}
|
||||
|
||||
|
||||
SqlSugarClient db = new SqlSugarClient(
|
||||
new ConnectionConfig()
|
||||
{
|
||||
ConnectionString = DbName,
|
||||
DbType = DbType.SqlServer,//设置数据库类型
|
||||
IsAutoCloseConnection = true,//自动释放数据务,如果存在事务,在事务结束后释放
|
||||
InitKeyType = InitKeyType.Attribute //从实体特性中读取主键自增列信息
|
||||
});
|
||||
var list = db.Queryable<ApplicationUser>().ToList();//查询所有
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// GET: api/User/5
|
||||
/// <summary>
|
||||
/// 获取用户详情根据token
|
||||
|
|
|
@ -7,6 +7,7 @@ using Blog.Core.Hubs;
|
|||
using Blog.Core.IServices;
|
||||
using Blog.Core.Middlewares;
|
||||
using Blog.Core.Model.Seed;
|
||||
using Blog.Core.SqlSugarDbRepository;
|
||||
using Blog.Core.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
@ -98,6 +99,8 @@ namespace Blog.Core
|
|||
//options.SerializerSettings.DateFormatString = "yyyy-MM-dd";
|
||||
});
|
||||
|
||||
services.AddSqlSugarDbStorage();
|
||||
|
||||
_services = services;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Blog.Core.Model\Blog.Core.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,66 @@
|
|||
using Blog.Core.SqlSugarDbRepository.Interface;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
|
||||
namespace Blog.Core.SqlSugarDbRepository
|
||||
{
|
||||
public class DefaultSqlSugarProviderStorage : ISqlSugarProviderStorage<ISqlSugarProvider>
|
||||
{
|
||||
public ConcurrentDictionary<string, ISqlSugarProvider> DataMap { get; private set; }
|
||||
|
||||
public DefaultSqlSugarProviderStorage(IServiceProvider serviceProvider)
|
||||
{
|
||||
DataMap = new ConcurrentDictionary<string, ISqlSugarProvider>();
|
||||
|
||||
var tmpDataMap = serviceProvider.GetServices<ISqlSugarProvider>()
|
||||
.ToDictionary(item => item.ProviderName);
|
||||
|
||||
foreach (var item in tmpDataMap)
|
||||
{
|
||||
this.AddOrUpdate(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
public void AddOrUpdate(string name, ISqlSugarProvider val)
|
||||
{
|
||||
DataMap[name] = val;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
DataMap.Clear();
|
||||
}
|
||||
|
||||
public ISqlSugarProvider GetByName(string name, string defaultName)
|
||||
{
|
||||
ISqlSugarProvider result = null;
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
if (!DataMap.TryGetValue(defaultName, out result))
|
||||
{
|
||||
throw new Exception("没有找到 DefaultName Provider");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else if (DataMap.TryGetValue(name, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"没有找到 {name} Provider");
|
||||
}
|
||||
|
||||
public void Remove(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.DataMap.TryRemove(name, out ISqlSugarProvider result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Blog.Core.SqlSugarDbRepository.Interface
|
||||
{
|
||||
public interface ISqlSugarProviderStorage<T> where T : ISqlSugarProvider
|
||||
{
|
||||
ConcurrentDictionary<string, T> DataMap { get; }
|
||||
|
||||
T GetByName(string name, string defaultName);
|
||||
|
||||
|
||||
void AddOrUpdate(string name, T val);
|
||||
|
||||
|
||||
void Remove(string name);
|
||||
|
||||
|
||||
void Clear();
|
||||
}
|
||||
|
||||
public interface ISqlSugarProvider : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 针对这个连接起别名
|
||||
/// </summary>
|
||||
string ProviderName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// SqlSugar实例
|
||||
/// </summary>
|
||||
SqlSugarClient Sugar { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Blog.Core.SqlSugarDbRepository.Interface
|
||||
{
|
||||
public interface ISqlSugarRepository<TEntity> where TEntity : class
|
||||
{
|
||||
/// <summary>
|
||||
/// 修改Provider
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
IDisposable ChangeProvider(string name);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 插入数据
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
int Insert(TEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// 查询数据
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
List<TEntity> GetQuery(Expression<Func<TEntity, bool>> expression = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取sqlSugar对象
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
SqlSugarClient GetCurrentSqlSugar();
|
||||
}
|
||||
|
||||
|
||||
}
|
27
Blog.Core.SqlSugarDbRepository/Interface/ISqlSugarSetting.cs
Normal file
27
Blog.Core.SqlSugarDbRepository/Interface/ISqlSugarSetting.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using SqlSugar;
|
||||
using System;
|
||||
|
||||
namespace Blog.Core.SqlSugarDbRepository.Interface
|
||||
{
|
||||
public interface ISqlSugarSetting
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 配置名称Kety
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
/// <summary>
|
||||
/// 数据库连接字符串
|
||||
/// </summary>
|
||||
string ConnectionString { get; set; }
|
||||
/// <summary>
|
||||
/// 数据库类型呢
|
||||
/// </summary>
|
||||
DbType DatabaseType { get; set; }
|
||||
/// <summary>
|
||||
/// 使用Sql执行日志
|
||||
/// </summary>
|
||||
Action<string, SugarParameter[]> LogExecuting { get; set; }
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
using Blog.Core.SqlSugarDbRepository.Interface;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace Blog.Core.SqlSugarDbRepository
|
||||
{
|
||||
public static class SqlSugarDbStorageServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddSqlSugarDbStorage(this IServiceCollection services
|
||||
//, ISqlSugarSetting defaultDbSetting
|
||||
)
|
||||
{
|
||||
//if (defaultDbSetting == null)
|
||||
//{
|
||||
// throw new ArgumentNullException(nameof(defaultDbSetting));
|
||||
//}
|
||||
|
||||
//services.AddSingleton<ISqlSugarProvider>(new SqlSugarProvider(defaultDbSetting));
|
||||
services.AddTransient(typeof(ISqlSugarRepository<>), typeof(SqlSugarRepository<>));
|
||||
services.AddSingleton<ISqlSugarProviderStorage<ISqlSugarProvider>, DefaultSqlSugarProviderStorage>();
|
||||
|
||||
return services;
|
||||
|
||||
}
|
||||
|
||||
public static IServiceProvider AddSqlSugarDatabaseProvider(this IServiceProvider serviceProvider, ISqlSugarSetting dbSetting)
|
||||
{
|
||||
if (dbSetting == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dbSetting));
|
||||
}
|
||||
|
||||
var fSqlProviderStorage = serviceProvider.GetRequiredService<ISqlSugarProviderStorage<ISqlSugarProvider>>();
|
||||
|
||||
fSqlProviderStorage.AddOrUpdate(dbSetting.Name, new SqlSugarProvider(dbSetting));
|
||||
|
||||
return serviceProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
40
Blog.Core.SqlSugarDbRepository/SqlSugarProvider.cs
Normal file
40
Blog.Core.SqlSugarDbRepository/SqlSugarProvider.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Blog.Core.SqlSugarDbRepository.Interface;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Blog.Core.SqlSugarDbRepository
|
||||
{
|
||||
public class SqlSugarProvider : ISqlSugarProvider
|
||||
{
|
||||
public string ProviderName { get; set; }
|
||||
public SqlSugarClient Sugar { get; set; }
|
||||
|
||||
public SqlSugarProvider(ISqlSugarSetting SugarSetting)
|
||||
{
|
||||
this.Sugar = this.CreateSqlSugar(SugarSetting);
|
||||
this.ProviderName = SugarSetting.Name;
|
||||
}
|
||||
|
||||
private SqlSugarClient CreateSqlSugar(ISqlSugarSetting SugarSetting)
|
||||
{
|
||||
|
||||
var db = new SqlSugarClient(
|
||||
new ConnectionConfig()
|
||||
{
|
||||
ConnectionString = SugarSetting.ConnectionString,
|
||||
DbType = SugarSetting.DatabaseType,//设置数据库类型
|
||||
IsAutoCloseConnection = true,//自动释放数据务,如果存在事务,在事务结束后释放
|
||||
InitKeyType = InitKeyType.Attribute //从实体特性中读取主键自增列信息
|
||||
});
|
||||
|
||||
//用来打印Sql方便你调式
|
||||
db.Aop.OnLogExecuting = SugarSetting.LogExecuting;
|
||||
return db;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.Sugar.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
84
Blog.Core.SqlSugarDbRepository/SqlSugarRepository.cs
Normal file
84
Blog.Core.SqlSugarDbRepository/SqlSugarRepository.cs
Normal file
|
@ -0,0 +1,84 @@
|
|||
using Blog.Core.SqlSugarDbRepository.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
|
||||
namespace Blog.Core.SqlSugarDbRepository
|
||||
{
|
||||
public class SqlSugarRepository<TEntity> : ISqlSugarRepository<TEntity>
|
||||
where TEntity : class, new()
|
||||
{
|
||||
|
||||
public string ProviderName { get; private set; }
|
||||
public string OldProviderName { get; private set; }
|
||||
protected readonly ISqlSugarProviderStorage<ISqlSugarProvider> _sqlSugarProviderStorage;
|
||||
|
||||
public SqlSugarRepository(ISqlSugarProviderStorage<ISqlSugarProvider> sqlSugarProviderStorage)
|
||||
{
|
||||
_sqlSugarProviderStorage = sqlSugarProviderStorage;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public IDisposable ChangeProvider(string name)
|
||||
{
|
||||
OldProviderName = ProviderName;
|
||||
ProviderName = name;
|
||||
return new DisposeAction(() =>
|
||||
{
|
||||
ProviderName = OldProviderName;
|
||||
OldProviderName = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public SqlSugarClient GetCurrentSqlSugar()
|
||||
{
|
||||
return this._sqlSugarProviderStorage.GetByName(this.ProviderName, SqlSugarDbStorageConsts.DefaultProviderName).Sugar;
|
||||
}
|
||||
|
||||
public int Insert(TEntity entity)
|
||||
{
|
||||
return this.GetCurrentSqlSugar().Insertable<TEntity>(entity).ExecuteCommand();
|
||||
}
|
||||
|
||||
public List<TEntity> GetQuery(Expression<Func<TEntity, bool>> expression = null)
|
||||
{
|
||||
return this.GetCurrentSqlSugar().Queryable<TEntity>().Where(expression).ToList();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class DisposeAction : IDisposable
|
||||
{
|
||||
public static readonly DisposeAction Empty = new DisposeAction(null);
|
||||
|
||||
private Action _action;
|
||||
|
||||
|
||||
public DisposeAction(Action action)
|
||||
{
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
var action = Interlocked.Exchange(ref _action, null);
|
||||
action?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public class SqlSugarDbStorageConsts
|
||||
{
|
||||
public static string DefaultProviderName = "DefaultProviderName";
|
||||
}
|
||||
|
||||
}
|
14
Blog.Core.SqlSugarDbRepository/SqlSugarSetting.cs
Normal file
14
Blog.Core.SqlSugarDbRepository/SqlSugarSetting.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using Blog.Core.SqlSugarDbRepository.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
|
||||
namespace Blog.Core.SqlSugarDbRepository
|
||||
{
|
||||
public class SqlSugarSetting : ISqlSugarSetting
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string ConnectionString { get; set; }
|
||||
public DbType DatabaseType { get; set; }
|
||||
public Action<string, SugarParameter[]> LogExecuting { get; set; }
|
||||
}
|
||||
}
|
|
@ -43,6 +43,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{EDA8901E
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blog.Core.Tests", "Blog.Core.Tests\Blog.Core.Tests.csproj", "{300A8113-8033-4184-BE28-FC48D8349CD0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blog.Core.SqlSugarDbRepository", "Blog.Core.SqlSugarDbRepository\Blog.Core.SqlSugarDbRepository.csproj", "{B27F8909-3F98-4289-A06F-EB7AF6952E2C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -97,6 +99,10 @@ Global
|
|||
{300A8113-8033-4184-BE28-FC48D8349CD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{300A8113-8033-4184-BE28-FC48D8349CD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{300A8113-8033-4184-BE28-FC48D8349CD0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B27F8909-3F98-4289-A06F-EB7AF6952E2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B27F8909-3F98-4289-A06F-EB7AF6952E2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B27F8909-3F98-4289-A06F-EB7AF6952E2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B27F8909-3F98-4289-A06F-EB7AF6952E2C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
Loading…
Reference in New Issue
Block a user