38 changed files with 840 additions and 253 deletions
@ -0,0 +1,14 @@ |
|||||
|
// Global using directives
|
||||
|
|
||||
|
global using System.Collections.Concurrent; |
||||
|
global using System.Runtime.Serialization; |
||||
|
global using Elasticsearch.Net; |
||||
|
global using Microsoft.Extensions.Logging; |
||||
|
global using Microsoft.Extensions.Options; |
||||
|
global using Nest; |
||||
|
global using Volo.Abp.Autofac; |
||||
|
global using Volo.Abp.DependencyInjection; |
||||
|
global using Volo.Abp.Modularity; |
||||
|
global using Microsoft.Extensions.DependencyInjection; |
||||
|
global using Volo.Abp; |
||||
|
global using LogLevel = Microsoft.Extensions.Logging.LogLevel; |
||||
@ -0,0 +1,13 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net7.0</TargetFramework> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="NEST" /> |
||||
|
<PackageReference Include="Volo.Abp.Autofac" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch; |
||||
|
|
||||
|
[DependsOn(typeof(AbpAutofacModule))] |
||||
|
public class AbpProElasticSearchModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
context.Services.Configure<AbpProElasticSearchOptions>(context.Services.GetConfiguration().GetSection("ElasticSearch")); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch; |
||||
|
|
||||
|
public class AbpProElasticSearchOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// es地址
|
||||
|
/// </summary>
|
||||
|
public string Host { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 用户名
|
||||
|
/// </summary>
|
||||
|
public string UserName { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 密码
|
||||
|
/// </summary>
|
||||
|
public string Password { get; set; } |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
using Lion.AbpPro.ElasticSearch.Exceptions; |
||||
|
|
||||
|
namespace Lion.AbpPro.ElasticSearch; |
||||
|
|
||||
|
public abstract class ElasticSearchRepository<TEntity> : IBasicElasticSearchRepository<TEntity> |
||||
|
where TEntity : class, IElasticSearchEntity |
||||
|
{ |
||||
|
protected abstract string IndexName { get; } |
||||
|
|
||||
|
private readonly IElasticsearchProvider _elasticsearchProvider; |
||||
|
|
||||
|
protected ElasticSearchRepository(IElasticsearchProvider elasticsearchProvider) |
||||
|
{ |
||||
|
_elasticsearchProvider = elasticsearchProvider; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
protected IElasticClient Client => _elasticsearchProvider.GetClient(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 根据id查询实体
|
||||
|
/// </summary>
|
||||
|
public virtual async Task<TEntity> FindAsync(Guid id, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var result = await Client.GetAsync<TEntity>(id, e => e.Index(IndexName), cancellationToken); |
||||
|
if (!result.IsValid) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return result.Source; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 根据id查询实体
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchEntityNotFoundException"></exception>
|
||||
|
/// <returns>如果没有查询到,会抛异常</returns>
|
||||
|
public virtual async Task<TEntity> GetAsync(Guid id, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var result = await Client.GetAsync<TEntity>(id, e => e.Index(IndexName), cancellationToken); |
||||
|
if (!result.IsValid) |
||||
|
{ |
||||
|
throw new AbpProElasticSearchEntityNotFoundException(innerException: result.OriginalException); |
||||
|
} |
||||
|
|
||||
|
return result.Source; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 新增数据时,如果文档的唯一id在索引里已存在,那么会更新掉原数据
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchException"></exception>
|
||||
|
public virtual async Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var result = await Client.IndexAsync(entity, x => x.Index(IndexName), cancellationToken); |
||||
|
if (!result.IsValid) |
||||
|
{ |
||||
|
throw new AbpProElasticSearchException(innerException: result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 批量新增数据时,如果文档的唯一id在索引里已存在,那么会更新掉原数据
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchException"></exception>
|
||||
|
public async Task InsertManyAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
// var result = await Client.BulkAsync(b => b.Index(IndexName).IndexMany(entities), cancellationToken);
|
||||
|
var result = await Client.IndexManyAsync(entities, IndexName, cancellationToken); |
||||
|
if (!result.IsValid) |
||||
|
{ |
||||
|
throw new AbpProElasticSearchException(innerException: result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 根据Id删除实体
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchException"></exception>
|
||||
|
public virtual async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var result = await Client.DeleteAsync<TEntity>(id, x => x.Index(IndexName), cancellationToken); |
||||
|
if (!result.IsValid) |
||||
|
{ |
||||
|
throw new AbpProElasticSearchException(innerException: result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 根据Id更新实体
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchException"></exception>
|
||||
|
public virtual async Task UpdateAsync(TEntity TEntity) |
||||
|
{ |
||||
|
var result = await Client.UpdateAsync<TEntity>(TEntity.Id, x => x.Index(IndexName).Doc(TEntity)); |
||||
|
if (!result.IsValid) |
||||
|
{ |
||||
|
throw new UserFriendlyException(result.OriginalException.Message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public async Task<Tuple<long, IList<TEntity>>> PageAsync(List<Func<QueryContainerDescriptor<TEntity>, QueryContainer>> predicates, int pageIndex = 1, int pageSize = 10) |
||||
|
{ |
||||
|
predicates ??= new List<Func<QueryContainerDescriptor<TEntity>, QueryContainer>>(); |
||||
|
var query = await Client.SearchAsync<TEntity>(x => x.Index(IndexName) |
||||
|
.Query(q => q.Bool(qb => qb.Filter(predicates))) |
||||
|
.From((pageIndex - 1) * pageSize) |
||||
|
.Size(pageSize) |
||||
|
.Sort(s => s.Descending(v => v.CreationTime))); |
||||
|
return new Tuple<long, IList<TEntity>>(query.Total, query.Documents.ToList()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch; |
||||
|
|
||||
|
public class ElasticsearchProvider : IElasticsearchProvider, ISingletonDependency |
||||
|
{ |
||||
|
private readonly AbpProElasticSearchOptions _options; |
||||
|
|
||||
|
public ElasticsearchProvider(IOptions<AbpProElasticSearchOptions> options) |
||||
|
{ |
||||
|
_options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public virtual IElasticClient GetClient() |
||||
|
{ |
||||
|
var connectionPool = new SingleNodeConnectionPool(new Uri(_options.Host)); |
||||
|
var settings = new ConnectionSettings(connectionPool); |
||||
|
settings.BasicAuthentication(_options.UserName, _options.Password); |
||||
|
return new ElasticClient(settings); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch.Exceptions; |
||||
|
|
||||
|
public class AbpProElasticSearchEntityNotFoundException : BusinessException |
||||
|
{ |
||||
|
public AbpProElasticSearchEntityNotFoundException( |
||||
|
string code = null, |
||||
|
string message = null, |
||||
|
string details = null, |
||||
|
Exception innerException = null, |
||||
|
LogLevel logLevel = LogLevel.Error) |
||||
|
: base( |
||||
|
code, |
||||
|
message, |
||||
|
details, |
||||
|
innerException, |
||||
|
logLevel |
||||
|
) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public AbpProElasticSearchEntityNotFoundException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch.Exceptions; |
||||
|
|
||||
|
public class AbpProElasticSearchException : BusinessException |
||||
|
{ |
||||
|
public AbpProElasticSearchException( |
||||
|
string code = null, |
||||
|
string message = null, |
||||
|
string details = null, |
||||
|
Exception innerException = null, |
||||
|
LogLevel logLevel = LogLevel.Error) |
||||
|
: base( |
||||
|
code, |
||||
|
message, |
||||
|
details, |
||||
|
innerException, |
||||
|
logLevel |
||||
|
) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public AbpProElasticSearchException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
using Lion.AbpPro.ElasticSearch.Exceptions; |
||||
|
|
||||
|
namespace Lion.AbpPro.ElasticSearch; |
||||
|
|
||||
|
public interface IBasicElasticSearchRepository<TEntity> where TEntity : class, IElasticSearchEntity |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 根据id查询实体
|
||||
|
/// </summary>
|
||||
|
Task<TEntity> FindAsync(Guid id, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 根据id查询实体
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchEntityNotFoundException"></exception>
|
||||
|
/// <returns>如果没有查询到,会抛异常</returns>
|
||||
|
Task<TEntity> GetAsync(Guid id, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 新增数据时,如果文档的唯一id在索引里已存在,那么会更新掉原数据
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchException"></exception>
|
||||
|
Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 批量新增数据时,如果文档的唯一id在索引里已存在,那么会更新掉原数据
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchException"></exception>
|
||||
|
Task InsertManyAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 根据Id删除实体
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchException"></exception>
|
||||
|
Task UpdateAsync(TEntity TEntity); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 根据Id更新实体
|
||||
|
/// </summary>
|
||||
|
/// <exception cref="AbpProElasticSearchException"></exception>
|
||||
|
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<Tuple<long, IList<TEntity>>> PageAsync(List<Func<QueryContainerDescriptor<TEntity>, QueryContainer>> predicates, int pageIndex = 1, int pageSize = 10); |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch; |
||||
|
|
||||
|
|
||||
|
public interface IElasticSearchEntity |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 主键Id
|
||||
|
/// </summary>
|
||||
|
Guid Id { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 创建时间
|
||||
|
/// </summary>
|
||||
|
DateTime CreationTime { get; set; } |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch; |
||||
|
|
||||
|
public interface IElasticsearchProvider |
||||
|
{ |
||||
|
IElasticClient GetClient(); |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net7.0</TargetFramework> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<IsPackable>false</IsPackable> |
||||
|
<IsTestProject>true</IsTestProject> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
||||
|
<PackageReference Include="NSubstitute" /> |
||||
|
<PackageReference Include="Shouldly" /> |
||||
|
<PackageReference Include="xunit" /> |
||||
|
<PackageReference Include="xunit.extensibility.execution" /> |
||||
|
<PackageReference Include="xunit.runner.visualstudio" /> |
||||
|
<PackageReference Include="coverlet.collector" /> |
||||
|
<PackageReference Include="JunitXml.TestLogger" /> |
||||
|
<PackageReference Include="Volo.Abp.TestBase" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Lion.AbpPro.ElasticSearch\Lion.AbpPro.ElasticSearch.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Content Include="appsettings.json"> |
||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory> |
||||
|
</Content> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,13 @@ |
|||||
|
using Volo.Abp.Testing; |
||||
|
|
||||
|
namespace Lion.AbpPro.ElasticSearch |
||||
|
{ |
||||
|
|
||||
|
public abstract class AbpProElasticSearchTestBase : AbpIntegratedTest<AbpProElasticSearchTestBaseModule> |
||||
|
{ |
||||
|
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
||||
|
{ |
||||
|
options.UseAutofac(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch |
||||
|
{ |
||||
|
|
||||
|
[DependsOn(typeof(AbpProElasticSearchModule))] |
||||
|
[DependsOn(typeof(AbpTestBaseModule))] |
||||
|
public class AbpProElasticSearchTestBaseModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,412 @@ |
|||||
|
using Lion.AbpPro.ElasticSearch.Exceptions; |
||||
|
using Lion.AbpPro.ElasticSearch.Students; |
||||
|
using Nest; |
||||
|
using Shouldly; |
||||
|
|
||||
|
namespace Lion.AbpPro.ElasticSearch |
||||
|
{ |
||||
|
public sealed class StudentElasticSearchRepositoryTests : AbpProElasticSearchTestBase |
||||
|
{ |
||||
|
private readonly IStudentElasticSearchRepository _studentElasticSearchRepository; |
||||
|
|
||||
|
|
||||
|
public StudentElasticSearchRepositoryTests() |
||||
|
{ |
||||
|
_studentElasticSearchRepository = GetRequiredService<IStudentElasticSearchRepository>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindAsync_Should_Find_Student() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var student = new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "John Doe", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.3, |
||||
|
Gender = Gender.Man |
||||
|
}; |
||||
|
await _studentElasticSearchRepository.InsertAsync(student); |
||||
|
|
||||
|
// Act
|
||||
|
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
||||
|
|
||||
|
// Assert
|
||||
|
result.ShouldNotBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetAsync_Should_Exception() |
||||
|
{ |
||||
|
await Should.ThrowAsync<AbpProElasticSearchEntityNotFoundException>(async () => { await _studentElasticSearchRepository.GetAsync(Guid.NewGuid()); }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task InsertAsync_Should_Insert_Student() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var student = new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "John Doe", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.3, |
||||
|
Gender = Gender.Man |
||||
|
}; |
||||
|
|
||||
|
// Act
|
||||
|
await _studentElasticSearchRepository.InsertAsync(student); |
||||
|
|
||||
|
// Assert
|
||||
|
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
||||
|
result.ShouldNotBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task InsertAsync_Should_RepeatInsert_Student() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var student = new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "John Doe", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.3, |
||||
|
Gender = Gender.Man |
||||
|
}; |
||||
|
|
||||
|
// Act
|
||||
|
await _studentElasticSearchRepository.InsertAsync(student); |
||||
|
|
||||
|
// Assert
|
||||
|
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
||||
|
result.ShouldNotBeNull(); |
||||
|
|
||||
|
// Act
|
||||
|
student.Name = "abp"; |
||||
|
student.Age = 20; |
||||
|
student.Gender = Gender.WoMan; |
||||
|
await _studentElasticSearchRepository.InsertAsync(student); |
||||
|
|
||||
|
// Assert
|
||||
|
var result1 = await _studentElasticSearchRepository.FindAsync(student.Id); |
||||
|
result1.ShouldNotBeNull(); |
||||
|
result1.Name.ShouldBe(student.Name); |
||||
|
result1.Age.ShouldBe(student.Age); |
||||
|
result1.Gender.ShouldBe(student.Gender); |
||||
|
result1.Price.ShouldBe(student.Price); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task InsertManyAsync_Should_Insert_Student() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var students = new List<Student> |
||||
|
{ |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "John Doe", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.3, |
||||
|
Gender = Gender.Man |
||||
|
}, |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "John Wang", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.3, |
||||
|
Gender = Gender.WoMan |
||||
|
} |
||||
|
}; |
||||
|
// Act
|
||||
|
await _studentElasticSearchRepository.InsertManyAsync(students); |
||||
|
|
||||
|
// Assert
|
||||
|
foreach (var student in students) |
||||
|
{ |
||||
|
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
||||
|
result.ShouldNotBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task DeleteAsync_Should_Delete_Student() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var student = new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "John Doe", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.3, |
||||
|
Gender = Gender.Man |
||||
|
}; |
||||
|
await _studentElasticSearchRepository.InsertAsync(student); |
||||
|
|
||||
|
// Act
|
||||
|
await _studentElasticSearchRepository.DeleteAsync(student.Id); |
||||
|
|
||||
|
// Assert
|
||||
|
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task UpdateAsync_Should_Update_Student() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var student = new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "John Doe", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.3, |
||||
|
Gender = Gender.Man |
||||
|
}; |
||||
|
await _studentElasticSearchRepository.InsertAsync(student); |
||||
|
|
||||
|
// Act
|
||||
|
student.Name = "update"; |
||||
|
student.Age = 20; |
||||
|
await _studentElasticSearchRepository.UpdateAsync(student); |
||||
|
|
||||
|
// Assert
|
||||
|
var exiStudent = await _studentElasticSearchRepository.FindAsync(student.Id); |
||||
|
exiStudent.ShouldNotBeNull(); |
||||
|
exiStudent.Name.ShouldBe(student.Name); |
||||
|
exiStudent.Age.ShouldBe(student.Age); |
||||
|
} |
||||
|
|
||||
|
[Fact(DisplayName = "DataRange时间范围查询")] |
||||
|
public async Task PageAsync_Should_Return_Students() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var students = new List<Student> |
||||
|
{ |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "韩立", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.1, |
||||
|
Gender = Gender.Man |
||||
|
}, |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "南宫婉", |
||||
|
Age = 18, |
||||
|
CreationTime = DateTime.Now.AddDays(-1), |
||||
|
Price = 100.2, |
||||
|
Gender = Gender.WoMan |
||||
|
}, |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "Test001", |
||||
|
Age = 19, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100, |
||||
|
Gender = Gender.WoMan |
||||
|
}, |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "Test", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now.AddDays(-10), |
||||
|
Price = 100, |
||||
|
Gender = Gender.WoMan |
||||
|
} |
||||
|
}; |
||||
|
// Act
|
||||
|
await _studentElasticSearchRepository.InsertManyAsync(students); |
||||
|
var TimeZone = "Asia/Shanghai"; |
||||
|
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
||||
|
|
||||
|
// // 查询日期
|
||||
|
// mustFilters.Add(e =>
|
||||
|
// e.DateRange(f =>
|
||||
|
// f.Field(fd => fd.CreationTime)
|
||||
|
// .TimeZone(TimeZone)
|
||||
|
// // 小于等于LessThanOrEquals
|
||||
|
// // 大于等于GreaterThanOrEquals
|
||||
|
// .GreaterThanOrEquals(DateTime.Now.AddDays(-1))));
|
||||
|
//
|
||||
|
// 查询日期区间 3天之前到现在
|
||||
|
mustFilters.Add(a => a |
||||
|
.Bool(b => b |
||||
|
.Must( |
||||
|
m => m.DateRange(r => r.Field(f => f.CreationTime).TimeZone(TimeZone).GreaterThanOrEquals(DateTime.Now.AddDays(-3))), |
||||
|
m => m.DateRange(r => r.Field(f => f.CreationTime).TimeZone(TimeZone).LessThanOrEquals(DateTime.Now)) |
||||
|
) |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
var query = new QueryContainerDescriptor<Student>(); |
||||
|
// https://zhuanlan.zhihu.com/p/592767668
|
||||
|
|
||||
|
// Act
|
||||
|
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotNull(result); |
||||
|
Assert.True(result.Item1 >= 0); |
||||
|
Assert.NotNull(result.Item2); |
||||
|
} |
||||
|
|
||||
|
[Fact(DisplayName = "Term精准查询")] |
||||
|
public async Task PageAsync_Term_Should_OK() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var students = new List<Student> |
||||
|
{ |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "韩立", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.1, |
||||
|
Gender = Gender.Man |
||||
|
}, |
||||
|
}; |
||||
|
await _studentElasticSearchRepository.InsertManyAsync(students); |
||||
|
// Act
|
||||
|
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
||||
|
// 因为text类型会自动生成keyword类型,所以此时这样可以查询出来
|
||||
|
mustFilters.Add(e => e.Term(f => f.Field(b => b.Name.Suffix("keyword")).Value("韩立"))); |
||||
|
// Act
|
||||
|
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotNull(result); |
||||
|
Assert.True(result.Item1 >= 0); |
||||
|
Assert.NotNull(result.Item2); |
||||
|
|
||||
|
// Act
|
||||
|
var mustFilters1 = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
||||
|
// 如果name是中文是无法查询到的,text类型会自动分词
|
||||
|
mustFilters1.Add(e => e.Term(f => f.Field(b => b.Name).Value("韩立"))); |
||||
|
// Act
|
||||
|
var result1 = await _studentElasticSearchRepository.PageAsync(mustFilters1); |
||||
|
|
||||
|
Assert.True(result1.Item1 == 0); |
||||
|
Assert.True(result1.Item2.Count == 0); |
||||
|
} |
||||
|
|
||||
|
[Fact(DisplayName = "Wildcard模糊查询")] |
||||
|
public async Task PageAsync_Wildcard_Should_OK() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var students = new List<Student> |
||||
|
{ |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "Mock", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.1, |
||||
|
Gender = Gender.Man |
||||
|
}, |
||||
|
}; |
||||
|
await _studentElasticSearchRepository.InsertManyAsync(students); |
||||
|
|
||||
|
// Act
|
||||
|
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
||||
|
// * 代表匹配多个字符
|
||||
|
// ?代表匹配单个字符
|
||||
|
mustFilters.Add(e => e.Wildcard(f => f.Field(b => b.Name).Value("Moc?"))); |
||||
|
// Act
|
||||
|
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotNull(result); |
||||
|
Assert.True(result.Item1 >= 0); |
||||
|
Assert.NotNull(result.Item2); |
||||
|
} |
||||
|
|
||||
|
[Fact(DisplayName = "Match查询")] |
||||
|
public async Task PageAsync_Match_Should_OK() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var students = new List<Student> |
||||
|
{ |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
//Name = "杀人防火历飞雨,万人敬仰韩天尊",
|
||||
|
Name = "Student1", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.1, |
||||
|
Gender = Gender.Man |
||||
|
}, |
||||
|
}; |
||||
|
await _studentElasticSearchRepository.InsertManyAsync(students); |
||||
|
|
||||
|
// Act
|
||||
|
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
||||
|
|
||||
|
// Student1 不会进行分词, 所以参数是Student 或者 1 都会查询不到数据
|
||||
|
mustFilters.Add(e => e.Match(f => f.Field(b => b.Name).Query("Student"))); |
||||
|
// Act
|
||||
|
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.True(result.Item1 == 0); |
||||
|
Assert.True(result.Item2.Count == 0); |
||||
|
} |
||||
|
|
||||
|
[Fact(DisplayName = "数值区间查询")] |
||||
|
public async Task PageAsync_Range_Should_OK() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var students = new List<Student> |
||||
|
{ |
||||
|
new Student |
||||
|
{ |
||||
|
Id = Guid.NewGuid(), |
||||
|
Name = "My Name Is Match", |
||||
|
Age = 10, |
||||
|
CreationTime = DateTime.Now, |
||||
|
Price = 100.1, |
||||
|
Gender = Gender.Man |
||||
|
}, |
||||
|
}; |
||||
|
await _studentElasticSearchRepository.InsertManyAsync(students); |
||||
|
|
||||
|
// Act
|
||||
|
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
||||
|
|
||||
|
// 查询数值区间
|
||||
|
mustFilters.Add(a => a |
||||
|
.Bool(b => b |
||||
|
.Must( |
||||
|
m => m.Range(r => r.Field(f => f.Age).GreaterThanOrEquals(0)), |
||||
|
m => m.Range(r => r.Field(f => f.Age).LessThanOrEquals(20)) |
||||
|
) |
||||
|
) |
||||
|
); |
||||
|
// Act
|
||||
|
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotNull(result); |
||||
|
Assert.True(result.Item1 >= 0); |
||||
|
Assert.NotNull(result.Item2); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch.Students; |
||||
|
|
||||
|
public enum Gender |
||||
|
{ |
||||
|
Man = 10, |
||||
|
WoMan = 20 |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch.Students; |
||||
|
|
||||
|
public interface IStudentElasticSearchRepository : IBasicElasticSearchRepository<Student> |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
namespace Lion.AbpPro.ElasticSearch.Students; |
||||
|
|
||||
|
public class Student : IElasticSearchEntity |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public DateTime CreationTime { get; set; } |
||||
|
|
||||
|
public double Price { get; set; } |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public int Age { get; set; } |
||||
|
|
||||
|
public Gender Gender { get; set; } |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Lion.AbpPro.ElasticSearch.Students; |
||||
|
|
||||
|
public class StudentElasticSearchRepository : ElasticSearchRepository<Student>, IStudentElasticSearchRepository, ITransientDependency |
||||
|
{ |
||||
|
public StudentElasticSearchRepository(IElasticsearchProvider elasticsearchProvider) : base(elasticsearchProvider) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
// index 只能是小写
|
||||
|
protected override string IndexName => "Students20230701".ToLower(); |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
global using Xunit; |
||||
|
global using Volo.Abp; |
||||
|
global using Volo.Abp.Modularity; |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"ElasticSearch": { |
||||
|
"Host": "http://localhost:9200", |
||||
|
"UserName": "admin", |
||||
|
"Password": "changeme" |
||||
|
} |
||||
|
} |
||||
@ -1,11 +0,0 @@ |
|||||
namespace Lion.AbpPro.ElasticSearches.Dto |
|
||||
{ |
|
||||
public class PagingElasticSearchLogInput : PagingBase |
|
||||
{ |
|
||||
public string Filter { get; set; } |
|
||||
|
|
||||
public DateTime? StartCreationTime { get; set; } |
|
||||
|
|
||||
public DateTime? EndCreationTime { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
namespace Lion.AbpPro.ElasticSearches.Dto |
|
||||
{ |
|
||||
|
|
||||
public class PagingElasticSearchLogOutput |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 日志级别
|
|
||||
/// </summary>
|
|
||||
public string Level { get; set; } |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 日志内容
|
|
||||
/// </summary>
|
|
||||
public string Message { get; set; } |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 创建时间
|
|
||||
/// </summary>
|
|
||||
public DateTime CreationTime { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,14 +0,0 @@ |
|||||
using Lion.AbpPro.ElasticSearches.Dto; |
|
||||
|
|
||||
namespace Lion.AbpPro.ElasticSearches |
|
||||
{ |
|
||||
public interface ILionAbpProLogAppService : IApplicationService |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 分页查询es日志
|
|
||||
/// </summary>
|
|
||||
/// <param name="input"></param>
|
|
||||
/// <returns></returns>
|
|
||||
Task<CustomPagedResultDto<PagingElasticSearchLogOutput>> PaingAsync(PagingElasticSearchLogInput input); |
|
||||
} |
|
||||
} |
|
||||
@ -1,29 +0,0 @@ |
|||||
namespace Lion.AbpPro.ElasticSearches.Dto |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Dto为什么在Service层
|
|
||||
/// 因为NEST 类库的坑 PropertyName必须用这个
|
|
||||
/// 不想在契约层添加NEST 包引用
|
|
||||
/// </summary>
|
|
||||
[Serializable] |
|
||||
public class PagingElasticSearchLogDto |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 日志级别
|
|
||||
/// </summary>
|
|
||||
public string Level { get; set; } |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 日志内容
|
|
||||
/// </summary>
|
|
||||
public string Message { get; set; } |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 创建时间
|
|
||||
/// </summary>
|
|
||||
[PropertyName("@timestamp")] |
|
||||
public DateTime CreationTime { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,10 +0,0 @@ |
|||||
namespace Lion.AbpPro.ElasticSearches |
|
||||
{ |
|
||||
public class ElasticSearchApplicationAutoMapperProfile : Profile |
|
||||
{ |
|
||||
public ElasticSearchApplicationAutoMapperProfile() |
|
||||
{ |
|
||||
CreateMap<PagingElasticSearchLogDto, PagingElasticSearchLogOutput>(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,73 +0,0 @@ |
|||||
namespace Lion.AbpPro.ElasticSearches |
|
||||
{ |
|
||||
[Authorize] |
|
||||
public class LionAbpProLogAppService : ElasticsearchBasicService, ILionAbpProLogAppService |
|
||||
{ |
|
||||
private readonly IConfiguration _configuration; |
|
||||
|
|
||||
// 时区
|
|
||||
private const string TimeZone = "Asia/Shanghai"; |
|
||||
|
|
||||
|
|
||||
public LionAbpProLogAppService( |
|
||||
IElasticsearchProvider elasticsearchProvider, |
|
||||
IConfiguration configuration) : base(elasticsearchProvider) |
|
||||
{ |
|
||||
_configuration = configuration; |
|
||||
} |
|
||||
|
|
||||
[Authorize(Policy = AbpProPermissions.SystemManagement.ES)] |
|
||||
public async Task<CustomPagedResultDto<PagingElasticSearchLogOutput>> PaingAsync(PagingElasticSearchLogInput input) |
|
||||
{ |
|
||||
var IndexName = _configuration.GetValue<string>("ElasticSearch:SearchIndexFormat"); |
|
||||
// 默认查询当天
|
|
||||
input.StartCreationTime = input.StartCreationTime?.AddMilliseconds(-1) ?? Clock.Now.Date.AddMilliseconds(-1); |
|
||||
input.EndCreationTime = input.EndCreationTime?.AddDays(1).AddMilliseconds(-1) ?? Clock.Now.Date.AddDays(1).AddMilliseconds(-1); |
|
||||
var mustFilters = new List<Func<QueryContainerDescriptor<PagingElasticSearchLogDto>, QueryContainer>>(); |
|
||||
if (input.StartCreationTime.HasValue) |
|
||||
{ |
|
||||
input.StartCreationTime = input.StartCreationTime.ToCurrentDateMaxDateTime(); |
|
||||
mustFilters.Add(e => e.DateRange(f => f.Field(fd => fd.CreationTime).TimeZone(TimeZone).GreaterThanOrEquals(input.StartCreationTime))); |
|
||||
} |
|
||||
|
|
||||
if (input.EndCreationTime.HasValue) |
|
||||
{ |
|
||||
input.EndCreationTime = input.EndCreationTime.ToNextSecondDateTime(); |
|
||||
mustFilters.Add(e => e.DateRange(f => f.Field(fd => fd.CreationTime).TimeZone(TimeZone).LessThanOrEquals(input.EndCreationTime))); |
|
||||
} |
|
||||
|
|
||||
if (!string.IsNullOrWhiteSpace(input.Filter)) |
|
||||
{ |
|
||||
mustFilters.Add |
|
||||
( |
|
||||
t => t.Match(f => f.Field(fd => fd.Message).Query(input.Filter.Trim()).Fuzziness(Fuzziness.Auto)) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
var result = await Client.SearchAsync<PagingElasticSearchLogDto> |
|
||||
( |
|
||||
e => e |
|
||||
.Index(IndexName) |
|
||||
.From(input.SkipCount) |
|
||||
.Size(input.PageSize) |
|
||||
.Sort(s => s.Descending(sd => sd.CreationTime)) |
|
||||
.Query(q => q.Bool(qb => qb.Filter(mustFilters))) |
|
||||
); |
|
||||
|
|
||||
if (result.HitsMetadata != null) |
|
||||
{ |
|
||||
return new CustomPagedResultDto<PagingElasticSearchLogOutput> |
|
||||
( |
|
||||
result.HitsMetadata.Total.Value, |
|
||||
ObjectMapper |
|
||||
.Map<List<PagingElasticSearchLogDto>, List<PagingElasticSearchLogOutput>> |
|
||||
( |
|
||||
result.Documents.ToList() |
|
||||
) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,15 +0,0 @@ |
|||||
namespace Lion.AbpPro.ElasticSearches.Providers |
|
||||
{ |
|
||||
public abstract class ElasticsearchBasicService : AbpProAppService |
|
||||
{ |
|
||||
private readonly IElasticsearchProvider _elasticsearchProvider; |
|
||||
|
|
||||
// ReSharper disable once PublicConstructorInAbstractClass
|
|
||||
public ElasticsearchBasicService(IElasticsearchProvider elasticsearchProvider) |
|
||||
{ |
|
||||
_elasticsearchProvider = elasticsearchProvider; |
|
||||
} |
|
||||
|
|
||||
protected IElasticClient Client => _elasticsearchProvider.GetElasticClient(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
namespace Lion.AbpPro.ElasticSearches.Providers |
|
||||
{ |
|
||||
public class ElasticsearchProvider : IElasticsearchProvider, ISingletonDependency |
|
||||
{ |
|
||||
private readonly IConfiguration _configuration; |
|
||||
|
|
||||
public ElasticsearchProvider(IConfiguration configuration) |
|
||||
{ |
|
||||
_configuration = configuration; |
|
||||
} |
|
||||
|
|
||||
public IElasticClient GetElasticClient() |
|
||||
{ |
|
||||
var pool = new SingleNodeConnectionPool(new Uri(_configuration.GetValue<string>("ElasticSearch:Url"))); |
|
||||
var connectionSettings = |
|
||||
new ConnectionSettings(pool); |
|
||||
connectionSettings.EnableHttpCompression(); |
|
||||
connectionSettings.BasicAuthentication(_configuration.GetValue<string>("ElasticSearch:UserName"), |
|
||||
_configuration.GetValue<string>("ElasticSearch:Password")); |
|
||||
return new ElasticClient(connectionSettings); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,7 +0,0 @@ |
|||||
namespace Lion.AbpPro.ElasticSearches.Providers |
|
||||
{ |
|
||||
public interface IElasticsearchProvider : ISingletonDependency |
|
||||
{ |
|
||||
IElasticClient GetElasticClient(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,20 +0,0 @@ |
|||||
namespace Lion.AbpPro.Controllers.Systems |
|
||||
{ |
|
||||
[Route("EsLog")] |
|
||||
public class LionAbpProLogController: AbpProController,ILionAbpProLogAppService |
|
||||
{ |
|
||||
private readonly ILionAbpProLogAppService _companyNameAbpProLogAppService; |
|
||||
|
|
||||
public LionAbpProLogController(ILionAbpProLogAppService companyNameAbpProLogAppService) |
|
||||
{ |
|
||||
_companyNameAbpProLogAppService = companyNameAbpProLogAppService; |
|
||||
} |
|
||||
|
|
||||
[HttpPost("page")] |
|
||||
[SwaggerOperation(summary: "分页获取Es日志", Tags = new[] { "EsLog" })] |
|
||||
public Task<CustomPagedResultDto<PagingElasticSearchLogOutput>> PaingAsync(PagingElasticSearchLogInput input) |
|
||||
{ |
|
||||
return _companyNameAbpProLogAppService.PaingAsync(input); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue