testing with EF no changes required works quite well :)
This commit is contained in:
parent
f6241df9a5
commit
46422bca42
@ -60,14 +60,12 @@ namespace PoweredSoft.DynamicLinq.Dal.Configurations
|
|||||||
ToTable("Comment", schema);
|
ToTable("Comment", schema);
|
||||||
HasKey(t => t.Id);
|
HasKey(t => t.Id);
|
||||||
Property(t => t.Id).HasColumnName("Id").HasColumnType("bigint").IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
|
Property(t => t.Id).HasColumnName("Id").HasColumnType("bigint").IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
|
||||||
Property(t => t.ParentCommentId).HasColumnName("ParentCommentId").HasColumnType("bigint").IsOptional();
|
|
||||||
Property(t => t.PostId).HasColumnName("PostId").HasColumnType("bigint").IsRequired();
|
Property(t => t.PostId).HasColumnName("PostId").HasColumnType("bigint").IsRequired();
|
||||||
Property(t => t.DisplayName).HasColumnName("DisplayName").HasColumnType("nvarchar").HasMaxLength(100).IsRequired();
|
Property(t => t.DisplayName).HasColumnName("DisplayName").HasColumnType("nvarchar").HasMaxLength(100).IsRequired();
|
||||||
Property(t => t.Email).HasColumnName("Email").HasColumnType("nvarchar").IsOptional();
|
Property(t => t.Email).HasColumnName("Email").HasColumnType("nvarchar").IsOptional();
|
||||||
Property(t => t.CommentText).HasColumnName("CommentText").HasColumnType("nvarchar").HasMaxLength(255).IsOptional();
|
Property(t => t.CommentText).HasColumnName("CommentText").HasColumnType("nvarchar").HasMaxLength(255).IsOptional();
|
||||||
|
|
||||||
HasRequired(t => t.Post).WithMany(t => t.Comments).HasForeignKey(t => t.PostId).WillCascadeOnDelete(false);
|
HasRequired(t => t.Post).WithMany(t => t.Comments).HasForeignKey(t => t.PostId).WillCascadeOnDelete(false);
|
||||||
HasOptional(t => t.ParentComment).WithMany(t => t.Comments).HasForeignKey(t => t.ParentCommentId).WillCascadeOnDelete(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,10 @@ namespace PoweredSoft.DynamicLinq.Dal.Pocos
|
|||||||
public class Comment
|
public class Comment
|
||||||
{
|
{
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
public long? ParentCommentId { get; set; }
|
|
||||||
public long PostId { get; set; }
|
public long PostId { get; set; }
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public string CommentText { get; set; }
|
public string CommentText { get; set; }
|
||||||
|
|
||||||
public Comment ParentComment { get; set; }
|
|
||||||
public ICollection<Comment> Comments { get; set; }
|
|
||||||
public Post Post { get; set; }
|
public Post Post { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ using System.Threading.Tasks;
|
|||||||
namespace PoweredSoft.DynamicLinq.Test
|
namespace PoweredSoft.DynamicLinq.Test
|
||||||
{
|
{
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class ComplexQueryTest
|
public class ComplexQueriesTests
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ComplexQueryBuilder()
|
public void ComplexQueryBuilder()
|
||||||
@ -100,7 +100,7 @@ namespace PoweredSoft.DynamicLinq.Test
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestCreateFilterExpressionCheckNull()
|
public void TestAutomaticNullChecking()
|
||||||
{
|
{
|
||||||
var authors = new List<Author>()
|
var authors = new List<Author>()
|
||||||
{
|
{
|
||||||
@ -174,81 +174,5 @@ namespace PoweredSoft.DynamicLinq.Test
|
|||||||
Assert.AreEqual(1, query.Count());
|
Assert.AreEqual(1, query.Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public void TestCreateFilterExpression()
|
|
||||||
{
|
|
||||||
var authors = new List<Author>()
|
|
||||||
{
|
|
||||||
new Author
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
FirstName = "David",
|
|
||||||
LastName = "Lebee",
|
|
||||||
Posts = new List<Post>
|
|
||||||
{
|
|
||||||
new Post
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
AuthorId = 1,
|
|
||||||
Title = "Match",
|
|
||||||
Content = "ABC",
|
|
||||||
Comments = new List<Comment>()
|
|
||||||
{
|
|
||||||
new Comment()
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
DisplayName = "John Doe",
|
|
||||||
CommentText = "!@#$!@#!@#",
|
|
||||||
Email = "John.doe@me.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Post
|
|
||||||
{
|
|
||||||
Id = 2,
|
|
||||||
AuthorId = 1,
|
|
||||||
Title = "Match",
|
|
||||||
Content = "ABC",
|
|
||||||
Comments = new List<Comment>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Author
|
|
||||||
{
|
|
||||||
Id = 2,
|
|
||||||
FirstName = "Chuck",
|
|
||||||
LastName = "Norris",
|
|
||||||
Posts = new List<Post>
|
|
||||||
{
|
|
||||||
new Post
|
|
||||||
{
|
|
||||||
Id = 3,
|
|
||||||
AuthorId = 2,
|
|
||||||
Title = "Match",
|
|
||||||
Content = "ASD",
|
|
||||||
Comments = new List<Comment>()
|
|
||||||
},
|
|
||||||
new Post
|
|
||||||
{
|
|
||||||
Id = 4,
|
|
||||||
AuthorId = 2,
|
|
||||||
Title = "DontMatch",
|
|
||||||
Content = "ASD",
|
|
||||||
Comments = new List<Comment>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// the query.
|
|
||||||
var query = authors.AsQueryable();
|
|
||||||
|
|
||||||
var allExpression = QueryableHelpers.CreateFilterExpression<Author>("Posts.Title", ConditionOperators.Equal, "Match", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.All);
|
|
||||||
var anyExpression = QueryableHelpers.CreateFilterExpression<Author>("Posts.Title", ConditionOperators.Equal, "Match", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.Any);
|
|
||||||
var anyExpression2 = QueryableHelpers.CreateFilterExpression<Author>("Posts.Comments.Email", ConditionOperators.Equal, "John.doe@me.com", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.Any);
|
|
||||||
Assert.AreEqual(1, query.Count(allExpression));
|
|
||||||
Assert.AreEqual(2, query.Count(anyExpression));
|
|
||||||
Assert.AreEqual(1, query.Count(anyExpression2));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
172
PoweredSoft.DynamicLinq.Test/EntityFrameworkTests.cs
Normal file
172
PoweredSoft.DynamicLinq.Test/EntityFrameworkTests.cs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using PoweredSoft.DynamicLinq.Dal;
|
||||||
|
using PoweredSoft.DynamicLinq.Dal.Pocos;
|
||||||
|
using PoweredSoft.DynamicLinq.Extensions;
|
||||||
|
|
||||||
|
namespace PoweredSoft.DynamicLinq.Test
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class EntityFrameworkTests
|
||||||
|
{
|
||||||
|
public static string testConnectionString =>
|
||||||
|
"data source=(local); initial catalog=blogtests;persist security info=True; Integrated Security=SSPI;";
|
||||||
|
|
||||||
|
public static void SeedForTests(BlogContext context)
|
||||||
|
{
|
||||||
|
context.Authors.Add(new Author
|
||||||
|
{
|
||||||
|
FirstName = "David",
|
||||||
|
LastName = "Lebee",
|
||||||
|
Posts = new List<Post>()
|
||||||
|
{
|
||||||
|
new Post()
|
||||||
|
{
|
||||||
|
CreateTime = DateTimeOffset.Now,
|
||||||
|
PublishTime = DateTimeOffset.Now,
|
||||||
|
Title = "New project",
|
||||||
|
Content = "Lots of good things coming",
|
||||||
|
Comments = new List<Comment>()
|
||||||
|
{
|
||||||
|
new Comment()
|
||||||
|
{
|
||||||
|
DisplayName = "John Doe",
|
||||||
|
Email = "john.doe@me.com",
|
||||||
|
CommentText = "Very interesting",
|
||||||
|
},
|
||||||
|
new Comment()
|
||||||
|
{
|
||||||
|
DisplayName = "Nice Guy",
|
||||||
|
Email = "nice.guy@lol.com",
|
||||||
|
CommentText = "Best of luck!",
|
||||||
|
//Comments = new List<Comment>()
|
||||||
|
//{
|
||||||
|
// new Comment()
|
||||||
|
// {
|
||||||
|
// DisplayName = "David Lebee",
|
||||||
|
// Email = "david@poweredsoft.com",
|
||||||
|
// CommentText = "Thanks!"
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Post()
|
||||||
|
{
|
||||||
|
CreateTime = DateTimeOffset.Now,
|
||||||
|
PublishTime = null,
|
||||||
|
Title = "The future!",
|
||||||
|
Content = "Is Near"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
context.Authors.Add(new Author
|
||||||
|
{
|
||||||
|
FirstName = "Some",
|
||||||
|
LastName = "Dude",
|
||||||
|
Posts = new List<Post>()
|
||||||
|
{
|
||||||
|
new Post() {
|
||||||
|
CreateTime = DateTimeOffset.Now,
|
||||||
|
PublishTime = DateTimeOffset.Now,
|
||||||
|
Title = "The One",
|
||||||
|
Content = "And Only"
|
||||||
|
},
|
||||||
|
new Post()
|
||||||
|
{
|
||||||
|
CreateTime = DateTimeOffset.Now,
|
||||||
|
PublishTime = DateTimeOffset.Now,
|
||||||
|
Title = "The Two",
|
||||||
|
Content = "And Second"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestSimpleWhere()
|
||||||
|
{
|
||||||
|
var context = new BlogContext(testConnectionString);
|
||||||
|
SeedForTests(context);
|
||||||
|
|
||||||
|
var query = context.Authors.AsQueryable();
|
||||||
|
query = query.Where("FirstName", ConditionOperators.Equal, "David");
|
||||||
|
var author = query.FirstOrDefault();
|
||||||
|
Assert.IsNotNull(author);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestWhereAnd()
|
||||||
|
{
|
||||||
|
var context = new BlogContext(testConnectionString);
|
||||||
|
SeedForTests(context);
|
||||||
|
|
||||||
|
var query = context.Authors.AsQueryable();
|
||||||
|
query = query.Query(q => q
|
||||||
|
.Compare("FirstName", ConditionOperators.Equal, "David")
|
||||||
|
.And("LastName", ConditionOperators.Equal, "Lebee")
|
||||||
|
);
|
||||||
|
|
||||||
|
var author = query.FirstOrDefault();
|
||||||
|
Assert.IsNotNull(author);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestWhereOr()
|
||||||
|
{
|
||||||
|
var context = new BlogContext(testConnectionString);
|
||||||
|
SeedForTests(context);
|
||||||
|
|
||||||
|
var query = context.Authors.AsQueryable();
|
||||||
|
query = query.Query(q => q
|
||||||
|
.Compare("FirstName", ConditionOperators.Equal, "David")
|
||||||
|
.Or("FirstName", ConditionOperators.Equal, "Some")
|
||||||
|
);
|
||||||
|
|
||||||
|
var author = query.FirstOrDefault();
|
||||||
|
Assert.IsNotNull(author);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGoingThroughSimpleNav()
|
||||||
|
{
|
||||||
|
var context = new BlogContext(testConnectionString);
|
||||||
|
SeedForTests(context);
|
||||||
|
|
||||||
|
var query = context.Posts.AsQueryable();
|
||||||
|
query = query.Where("Author.FirstName", ConditionOperators.Contains, "David");
|
||||||
|
Assert.AreEqual(2, query.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGoingThroughCollectionNav()
|
||||||
|
{
|
||||||
|
var context = new BlogContext(testConnectionString);
|
||||||
|
SeedForTests(context);
|
||||||
|
|
||||||
|
var query = context.Authors.AsQueryable();
|
||||||
|
query = query.Where("Posts.Title", ConditionOperators.Contains, "New");
|
||||||
|
var author = query.FirstOrDefault();
|
||||||
|
|
||||||
|
Assert.AreEqual(author?.FirstName, "David");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGoingThrough2CollectionNav()
|
||||||
|
{
|
||||||
|
var context = new BlogContext(testConnectionString);
|
||||||
|
SeedForTests(context);
|
||||||
|
|
||||||
|
var query = context.Authors.AsQueryable();
|
||||||
|
query = query.Where("Posts.Comments.Email", ConditionOperators.Contains, "@me.com");
|
||||||
|
var author = query.FirstOrDefault();
|
||||||
|
|
||||||
|
Assert.AreEqual(author?.FirstName, "David");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
PoweredSoft.DynamicLinq.Test/HelpersTests.cs
Normal file
91
PoweredSoft.DynamicLinq.Test/HelpersTests.cs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using PoweredSoft.DynamicLinq.Dal.Pocos;
|
||||||
|
using PoweredSoft.DynamicLinq.Helpers;
|
||||||
|
|
||||||
|
namespace PoweredSoft.DynamicLinq.Test
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class HelpersTests
|
||||||
|
{
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestCreateFilterExpression()
|
||||||
|
{
|
||||||
|
var authors = new List<Author>()
|
||||||
|
{
|
||||||
|
new Author
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
FirstName = "David",
|
||||||
|
LastName = "Lebee",
|
||||||
|
Posts = new List<Post>
|
||||||
|
{
|
||||||
|
new Post
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
AuthorId = 1,
|
||||||
|
Title = "Match",
|
||||||
|
Content = "ABC",
|
||||||
|
Comments = new List<Comment>()
|
||||||
|
{
|
||||||
|
new Comment()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DisplayName = "John Doe",
|
||||||
|
CommentText = "!@#$!@#!@#",
|
||||||
|
Email = "John.doe@me.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Post
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
AuthorId = 1,
|
||||||
|
Title = "Match",
|
||||||
|
Content = "ABC",
|
||||||
|
Comments = new List<Comment>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Author
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
FirstName = "Chuck",
|
||||||
|
LastName = "Norris",
|
||||||
|
Posts = new List<Post>
|
||||||
|
{
|
||||||
|
new Post
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
AuthorId = 2,
|
||||||
|
Title = "Match",
|
||||||
|
Content = "ASD",
|
||||||
|
Comments = new List<Comment>()
|
||||||
|
},
|
||||||
|
new Post
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
AuthorId = 2,
|
||||||
|
Title = "DontMatch",
|
||||||
|
Content = "ASD",
|
||||||
|
Comments = new List<Comment>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// the query.
|
||||||
|
var query = authors.AsQueryable();
|
||||||
|
|
||||||
|
var allExpression = QueryableHelpers.CreateFilterExpression<Author>("Posts.Title", ConditionOperators.Equal, "Match", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.All);
|
||||||
|
var anyExpression = QueryableHelpers.CreateFilterExpression<Author>("Posts.Title", ConditionOperators.Equal, "Match", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.Any);
|
||||||
|
var anyExpression2 = QueryableHelpers.CreateFilterExpression<Author>("Posts.Comments.Email", ConditionOperators.Equal, "John.doe@me.com", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.Any);
|
||||||
|
Assert.AreEqual(1, query.Count(allExpression));
|
||||||
|
Assert.AreEqual(2, query.Count(anyExpression));
|
||||||
|
Assert.AreEqual(1, query.Count(anyExpression2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,12 @@
|
|||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Faker, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
<Reference Include="Faker, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
<HintPath>..\packages\Faker.Data.1.0.7\lib\net45\Faker.dll</HintPath>
|
<HintPath>..\packages\Faker.Data.1.0.7\lib\net45\Faker.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
@ -48,13 +54,16 @@
|
|||||||
<HintPath>..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
|
<HintPath>..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.ComponentModel.DataAnnotations" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="ComplexQueryTest.cs" />
|
<Compile Include="ComplexQueriesTests.cs" />
|
||||||
<Compile Include="QueryTests.cs" />
|
<Compile Include="SimpleQueriesTest.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="HelpersTests.cs" />
|
||||||
|
<Compile Include="EntityFrameworkTests.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
|
@ -9,7 +9,7 @@ using PoweredSoft.DynamicLinq.Extensions;
|
|||||||
namespace PoweredSoft.DynamicLinq.Test
|
namespace PoweredSoft.DynamicLinq.Test
|
||||||
{
|
{
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class QueryTests
|
public class SimpleQueryTests
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Equal()
|
public void Equal()
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
|
<package id="EntityFramework" version="6.2.0" targetFramework="net461" />
|
||||||
<package id="Faker.Data" version="1.0.7" targetFramework="net461" />
|
<package id="Faker.Data" version="1.0.7" targetFramework="net461" />
|
||||||
<package id="MSTest.TestAdapter" version="1.2.0" targetFramework="net461" />
|
<package id="MSTest.TestAdapter" version="1.2.0" targetFramework="net461" />
|
||||||
<package id="MSTest.TestFramework" version="1.2.0" targetFramework="net461" />
|
<package id="MSTest.TestFramework" version="1.2.0" targetFramework="net461" />
|
||||||
|
Loading…
Reference in New Issue
Block a user