diff --git a/src/MediatR.CommandQuery/Commands/EntityIdentifierCommand.cs b/src/MediatR.CommandQuery/Commands/EntityIdentifierCommand.cs index 9d9b6dc6..2da75ecd 100644 --- a/src/MediatR.CommandQuery/Commands/EntityIdentifierCommand.cs +++ b/src/MediatR.CommandQuery/Commands/EntityIdentifierCommand.cs @@ -1,8 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Claims; -using Microsoft.Extensions.Logging; - namespace MediatR.CommandQuery.Commands; public abstract record EntityIdentifierCommand diff --git a/src/MediatR.CommandQuery/Commands/EntityPatchCommand.cs b/src/MediatR.CommandQuery/Commands/EntityPatchCommand.cs index 17284015..ac9e69c4 100644 --- a/src/MediatR.CommandQuery/Commands/EntityPatchCommand.cs +++ b/src/MediatR.CommandQuery/Commands/EntityPatchCommand.cs @@ -4,8 +4,6 @@ using MediatR.CommandQuery.Definitions; using MediatR.CommandQuery.Services; -using Microsoft.Extensions.Logging; - using SystemTextJsonPatch; namespace MediatR.CommandQuery.Commands; diff --git a/src/MediatR.CommandQuery/Commands/EntityUpdateCommand.cs b/src/MediatR.CommandQuery/Commands/EntityUpdateCommand.cs index e3eff173..0657dfeb 100644 --- a/src/MediatR.CommandQuery/Commands/EntityUpdateCommand.cs +++ b/src/MediatR.CommandQuery/Commands/EntityUpdateCommand.cs @@ -4,8 +4,6 @@ using MediatR.CommandQuery.Definitions; using MediatR.CommandQuery.Services; -using Microsoft.Extensions.Logging; - namespace MediatR.CommandQuery.Commands; public record EntityUpdateCommand diff --git a/src/MediatR.CommandQuery/Definitions/ISupportSearch.cs b/src/MediatR.CommandQuery/Definitions/ISupportSearch.cs new file mode 100644 index 00000000..4a03b668 --- /dev/null +++ b/src/MediatR.CommandQuery/Definitions/ISupportSearch.cs @@ -0,0 +1,8 @@ +namespace MediatR.CommandQuery.Definitions; + +public interface ISupportSearch +{ + static abstract IEnumerable SearchFields(); + + static abstract string SortField(); +} diff --git a/src/MediatR.CommandQuery/Dispatcher/DispatcherDataService.cs b/src/MediatR.CommandQuery/Dispatcher/DispatcherDataService.cs new file mode 100644 index 00000000..e56672e9 --- /dev/null +++ b/src/MediatR.CommandQuery/Dispatcher/DispatcherDataService.cs @@ -0,0 +1,178 @@ +using System.Security.Claims; + +using MediatR.CommandQuery.Commands; +using MediatR.CommandQuery.Definitions; +using MediatR.CommandQuery.Queries; + +namespace MediatR.CommandQuery.Dispatcher; + +public class DispatcherDataService : IDispatcherDataService +{ + public DispatcherDataService(IDispatcher dispatcher) + { + ArgumentNullException.ThrowIfNull(dispatcher); + + Dispatcher = dispatcher; + } + + + public IDispatcher Dispatcher { get; } + + + public async Task Get( + TKey id, + TimeSpan? cacheTime = null, + CancellationToken cancellationToken = default) + where TModel : class + { + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntityIdentifierQuery(user, id); + command.Cache(cacheTime); + + return await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + } + + public async Task> Get( + IEnumerable ids, + TimeSpan? cacheTime = null, + CancellationToken cancellationToken = default) + where TModel : class + { + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntityIdentifiersQuery(user, ids); + command.Cache(cacheTime); + + var result = await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + return result ?? []; + } + + public async Task> All( + string? sortField = null, + TimeSpan? cacheTime = null, + CancellationToken cancellationToken = default) + where TModel : class + { + var filter = new EntityFilter(); + var sort = EntitySort.Parse(sortField); + + var select = new EntitySelect(filter, sort); + + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntitySelectQuery(user, select); + command.Cache(cacheTime); + + var result = await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + return result ?? []; + } + + public async Task> Select( + EntitySelect? entitySelect = null, + TimeSpan? cacheTime = null, + CancellationToken cancellationToken = default) + where TModel : class + { + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntitySelectQuery(user, entitySelect); + command.Cache(cacheTime); + + var result = await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + return result ?? []; + } + + public async Task> Page( + EntityQuery? entityQuery = null, + CancellationToken cancellationToken = default) + where TModel : class + { + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntityPagedQuery(user, entityQuery); + + var result = await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + + return result ?? new EntityPagedResult(); + } + + + public async Task> Search( + string searchText, + CancellationToken cancellationToken = default) + where TModel : class, ISupportSearch + { + var filter = EntityFilterBuilder.CreateSearchFilter(TModel.SearchFields(), searchText); + var sort = new EntitySort { Name = TModel.SortField() }; + + var select = new EntitySelect(filter, sort); + + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntitySelectQuery(user, select); + + var result = await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + return result ?? []; + } + + + public async Task Save( + TKey id, + TUpdateModel updateModel, + CancellationToken cancellationToken = default) + where TReadModel : class + where TUpdateModel : class + { + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntityUpsertCommand(user, id, updateModel); + + return await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + } + + public async Task Create( + TCreateModel createModel, + CancellationToken cancellationToken = default) + where TReadModel : class + where TCreateModel : class + { + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntityCreateCommand(user, createModel); + + return await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + } + + public async Task Update( + TKey id, + TUpdateModel updateModel, + CancellationToken cancellationToken = default) + where TReadModel : class + where TUpdateModel : class + { + var user = await GetUser(cancellationToken).ConfigureAwait(false); + + var command = new EntityUpdateCommand(user, id, updateModel); + + return await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + } + + public async Task Delete( + TKey id, + CancellationToken cancellationToken = default) + where TReadModel : class + { + var user = await GetUser(cancellationToken).ConfigureAwait(false); + var command = new EntityDeleteCommand(user, id); + + return await Dispatcher.Send(command, cancellationToken).ConfigureAwait(false); + } + + + public virtual Task GetUser(CancellationToken cancellationToken = default) + { + return Task.FromResult(null); + } + +} diff --git a/src/MediatR.CommandQuery/Dispatcher/IDispatcherDataService.cs b/src/MediatR.CommandQuery/Dispatcher/IDispatcherDataService.cs new file mode 100644 index 00000000..c45e5320 --- /dev/null +++ b/src/MediatR.CommandQuery/Dispatcher/IDispatcherDataService.cs @@ -0,0 +1,75 @@ +using System.Security.Claims; + +using MediatR.CommandQuery.Definitions; +using MediatR.CommandQuery.Queries; + +namespace MediatR.CommandQuery.Dispatcher; + +public interface IDispatcherDataService +{ + IDispatcher Dispatcher { get; } + + Task Get( + TKey id, + TimeSpan? cacheTime = null, + CancellationToken cancellationToken = default) + where TModel : class; + + Task> Get( + IEnumerable ids, + TimeSpan? cacheTime = null, + CancellationToken cancellationToken = default) + where TModel : class; + + Task> All( + string? sortField = null, + TimeSpan? cacheTime = null, + CancellationToken cancellationToken = default) + where TModel : class; + + Task> Select( + EntitySelect? entitySelect = null, + TimeSpan? cacheTime = null, + CancellationToken cancellationToken = default) + where TModel : class; + + Task> Page( + EntityQuery? entityQuery = null, + CancellationToken cancellationToken = default) + where TModel : class; + + + Task> Search( + string searchText, + CancellationToken cancellationToken = default) + where TModel : class, ISupportSearch; + + + + Task Save( + TKey id, + TUpdateModel updateModel, + CancellationToken cancellationToken = default) + where TUpdateModel : class + where TReadModel : class; + + Task Create( + TCreateModel createModel, + CancellationToken cancellationToken = default) + where TCreateModel : class + where TReadModel : class; + + Task Update( + TKey id, + TUpdateModel updateModel, + CancellationToken cancellationToken = default) + where TUpdateModel : class + where TReadModel : class; + + Task Delete( + TKey id, + CancellationToken cancellationToken = default) where TReadModel : class; + + + Task GetUser(CancellationToken cancellationToken = default); +} diff --git a/src/MediatR.CommandQuery/MediatorServiceExtensions.cs b/src/MediatR.CommandQuery/MediatorServiceExtensions.cs index db1a7dfa..2e597947 100644 --- a/src/MediatR.CommandQuery/MediatorServiceExtensions.cs +++ b/src/MediatR.CommandQuery/MediatorServiceExtensions.cs @@ -55,6 +55,8 @@ public static IServiceCollection AddRemoteDispatcher(this IServiceCollection ser services.TryAddTransient(sp => sp.GetRequiredService()); services.AddOptions(); + services.TryAddTransient(); + return services; } @@ -65,6 +67,8 @@ public static IServiceCollection AddServerDispatcher(this IServiceCollection ser services.TryAddTransient(); services.AddOptions(); + services.TryAddTransient(); + return services; } diff --git a/src/MediatR.CommandQuery/Queries/EntityFilterBuilder.cs b/src/MediatR.CommandQuery/Queries/EntityFilterBuilder.cs new file mode 100644 index 00000000..b9254400 --- /dev/null +++ b/src/MediatR.CommandQuery/Queries/EntityFilterBuilder.cs @@ -0,0 +1,64 @@ + +using MediatR.CommandQuery.Definitions; + +namespace MediatR.CommandQuery.Queries; + +public static class EntityFilterBuilder +{ + public static EntityQuery? CreateSearchQuery(string searchText, int page = 1, int pageSize = 20) + where TModel : class, ISupportSearch + { + var filter = CreateSearchFilter(searchText); + var sort = CreateSort(); + + return new EntityQuery(filter, sort, page, pageSize); + } + + public static EntitySelect? CreateSearchSelect(string searchText) + where TModel : class, ISupportSearch + { + var filter = CreateSearchFilter(searchText); + var sort = CreateSort(); + + return new EntitySelect(filter, sort); + } + + public static EntityFilter? CreateSearchFilter(string searchText) + where TModel : class, ISupportSearch + { + return CreateSearchFilter(TModel.SearchFields(), searchText); + } + + + public static EntitySort? CreateSort() + where TModel : class, ISupportSearch + { + return new EntitySort { Name = TModel.SortField() }; + } + + + public static EntityFilter? CreateSearchFilter(IEnumerable fields, string searchText) + { + if (fields is null || string.IsNullOrWhiteSpace(searchText)) + return null; + + var groupFilter = new EntityFilter + { + Logic = EntityFilterLogic.Or, + Filters = [], + }; + + foreach (var field in fields) + { + var filter = new EntityFilter + { + Name = field, + Value = searchText, + Operator = EntityFilterOperators.Contains, + }; + groupFilter.Filters.Add(filter); + } + + return groupFilter; + } +} diff --git a/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/DatabaseFixture.cs b/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/DatabaseFixture.cs index 368a4bb6..424ca546 100644 --- a/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/DatabaseFixture.cs +++ b/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/DatabaseFixture.cs @@ -24,6 +24,8 @@ protected override void ConfigureApplication(HostApplicationBuilder builder) services.AddMediator(); services.AddValidatorsFromAssembly(); + services.AddServerDispatcher(); + services.AddMediatRCommandQueryEntityFrameworkCoreSqlServerTests(); } } diff --git a/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/Dispatcher/DispatcherDataServiceTests.cs b/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/Dispatcher/DispatcherDataServiceTests.cs new file mode 100644 index 00000000..c46e4636 --- /dev/null +++ b/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/Dispatcher/DispatcherDataServiceTests.cs @@ -0,0 +1,65 @@ +using MediatR.CommandQuery.Dispatcher; +using MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests.Domain.Priority.Models; + +using Microsoft.Extensions.DependencyInjection; + +namespace MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests.Dispatcher; + +[Collection(DatabaseCollection.CollectionName)] +public class DispatcherDataServiceTests : DatabaseTestBase +{ + public DispatcherDataServiceTests(ITestOutputHelper output, DatabaseFixture databaseFixture) + : base(output, databaseFixture) + { + } + + [Fact] + [Trait("Category", "SqlServer")] + public async Task FullTest() + { + var dataService = ServiceProvider.GetService(); + dataService.Should().NotBeNull(); + + var generator = new Faker() + .RuleFor(p => p.Id, (faker, model) => Guid.NewGuid()) + .RuleFor(p => p.Created, (faker, model) => faker.Date.PastOffset()) + .RuleFor(p => p.CreatedBy, (faker, model) => faker.Internet.Email()) + .RuleFor(p => p.Updated, (faker, model) => faker.Date.SoonOffset()) + .RuleFor(p => p.UpdatedBy, (faker, model) => faker.Internet.Email()) + .RuleFor(p => p.Name, (faker, model) => faker.Name.JobType()) + .RuleFor(p => p.Description, (faker, model) => faker.Lorem.Sentence()); + + var createModel = generator.Generate(); + + var createResult = await dataService.Create(createModel); + createResult.Should().NotBeNull(); + createResult.Name.Should().Be(createModel.Name); + + var searchResult = await dataService.Search(createModel.Name); + searchResult.Should().NotBeNull(); + + var selectEmptyResult = await dataService.Select(); + selectEmptyResult.Should().NotBeNull(); + + var pageEmptyResult = await dataService.Page(); + pageEmptyResult.Should().NotBeNull(); + + var getReadResult = await dataService.Get(createResult.Id); + getReadResult.Should().NotBeNull(); + + var getMultipleResult = await dataService.Get([createResult.Id]); + getMultipleResult.Should().NotBeNull(); + + + var getUpdateResult = await dataService.Get(createResult.Id); + getUpdateResult.Should().NotBeNull(); + + getUpdateResult.Description = "This is an update"; + + var updateResult = await dataService.Update(createResult.Id, getUpdateResult); + updateResult.Should().NotBeNull(); + + var deleteResult = await dataService.Delete(createResult.Id); + deleteResult.Should().NotBeNull(); + } +} diff --git a/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/Domain/Priority/Models/PriorityReadModel.cs b/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/Domain/Priority/Models/PriorityReadModel.cs index 75c6ed6d..e315b6d0 100644 --- a/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/Domain/Priority/Models/PriorityReadModel.cs +++ b/test/MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests/Domain/Priority/Models/PriorityReadModel.cs @@ -1,7 +1,10 @@ + +using MediatR.CommandQuery.Definitions; + namespace MediatR.CommandQuery.EntityFrameworkCore.SqlServer.Tests.Domain.Priority.Models; public partial class PriorityReadModel - : EntityReadModel + : EntityReadModel, ISupportSearch { #region Generated Properties public string Name { get; set; } @@ -11,7 +14,9 @@ public partial class PriorityReadModel public int DisplayOrder { get; set; } public bool IsActive { get; set; } - #endregion + public static IEnumerable SearchFields() => [nameof(Name), nameof(Description)]; + + public static string SortField() => nameof(DisplayOrder); }