add command update name,email add health query and dal queryable

This commit is contained in:
DavidGudEnough 2025-01-03 15:38:14 -05:00
parent 1de67205e0
commit f8829cd8bd
Signed by: david.nguyen
GPG Key ID: 0B95DC36355BEB37
26 changed files with 413 additions and 52 deletions

View File

@ -9,7 +9,7 @@ public class AppModule : IModule
{ {
public IServiceCollection ConfigureServices(IServiceCollection services) public IServiceCollection ConfigureServices(IServiceCollection services)
{ {
//services.AddModule<DalModule>(); services.AddModule<DalModule>();
services.AddModule<SharedModule>(); services.AddModule<SharedModule>();
services.AddModule<CommandModule>(); services.AddModule<CommandModule>();
services.AddModule<QueryModule>(); services.AddModule<QueryModule>();

View File

@ -39,14 +39,16 @@ builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery(); builder.Services.AddDefaultQueryDiscovery();
builder.Services.AddFluentValidation(); builder.Services.AddFluentValidation();
builder.Services.AddModule<AppModule>(); builder.Services.AddModule<AppModule>();
builder.Services.AddDefaultCommandDiscovery(); builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery(); builder.Services.AddDefaultQueryDiscovery();
if (builder.Configuration.GetValue<bool>("Swagger:Enable")) if (builder.Configuration.GetValue<bool>("Swagger:Enable"))
{ {
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => builder.Services.AddSwaggerGen(options =>
{ {
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Plan B Route Service Api", Version = "0.1.0" }); options.SwaggerDoc("v1", new OpenApiInfo { Title = "Constellation Heating Api", Version = "0.1.0" });
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{ {
@ -105,6 +107,7 @@ mvcBuilder
mvcBuilder mvcBuilder
.AddOpenHarborQueries() .AddOpenHarborQueries()
.AddOpenHarborDynamicQueries(); .AddOpenHarborDynamicQueries();
var connectionString = builder.Configuration.GetSection("Database").GetValue<string>("ConnectionString"); var connectionString = builder.Configuration.GetSection("Database").GetValue<string>("ConnectionString");
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
@ -112,13 +115,7 @@ var dataSource = dataSourceBuilder.Build();
builder.Services.AddDbContextPool<CHDbContext>(options => builder.Services.AddDbContextPool<CHDbContext>(options =>
{ {
options.UseNpgsql(dataSource, o => options.UseNpgsql(dataSource);
{
// todo: ef 9.0+
// o.MapEnum<RouteFileType>("route_file_type");
});
if (builder.Configuration.GetValue<bool>("Debug")) if (builder.Configuration.GetValue<bool>("Debug"))
{ {
AppContext.SetSwitch("Npgsql.EnableConnectionStringLogging", true); AppContext.SetSwitch("Npgsql.EnableConnectionStringLogging", true);

View File

@ -21,7 +21,7 @@
"https": { "https": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": false,
"applicationUrl": "https://localhost:7184;http://localhost:5274", "applicationUrl": "https://localhost:7184;http://localhost:5274",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"

View File

@ -1,22 +1,18 @@
using CH.Dal; using CH.Dal;
using CH.Dal.DbEntity; using CH.Dal.DbEntity;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace CH.Authority.Services; namespace CH.Authority.Services;
public class UserIdentityService public class UserIdentityService
{ {
private User? _user; private User? user;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor httpContextAccessor;
private readonly IConfiguration _configuration; private readonly IConfiguration configuration;
private readonly CHDbContext _dbContext; private readonly CHDbContext dbContext;
public string? SubjectId { get; } public string? SubjectId { get; }
public string? Email { get; } public string? Email { get; }
@ -28,9 +24,9 @@ public class UserIdentityService
public UserIdentityService(CHDbContext dbContext, IHttpContextAccessor httpContextAccessor, public UserIdentityService(CHDbContext dbContext, IHttpContextAccessor httpContextAccessor,
IConfiguration configuration) IConfiguration configuration)
{ {
_dbContext = dbContext; this.dbContext = dbContext;
_httpContextAccessor = httpContextAccessor; this.httpContextAccessor = httpContextAccessor;
_configuration = configuration; this.configuration = configuration;
// microsoft you twats! NameIdentifier = Sub // microsoft you twats! NameIdentifier = Sub
SubjectId = ResolveClaim(ClaimTypes.NameIdentifier); SubjectId = ResolveClaim(ClaimTypes.NameIdentifier);
@ -48,7 +44,7 @@ public class UserIdentityService
public bool IsAuthenticated() public bool IsAuthenticated()
{ {
var isAuthenticated = _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false; var isAuthenticated = httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
if (!isAuthenticated || null == EmailVerified) if (!isAuthenticated || null == EmailVerified)
return false; return false;
@ -58,11 +54,11 @@ public class UserIdentityService
private string? ResolveClaim(params string[] name) private string? ResolveClaim(params string[] name)
{ {
var isAuthenticated = _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false; var isAuthenticated = httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
if (false == isAuthenticated) if (false == isAuthenticated)
return null; return null;
var claim = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(claim => name.Contains(claim.Type)); var claim = httpContextAccessor.HttpContext?.User.Claims.FirstOrDefault(claim => name.Contains(claim.Type));
return claim?.Value; return claim?.Value;
} }
public async Task<User?> GetUserOrDefaultAsync(CancellationToken cancellationToken = default) public async Task<User?> GetUserOrDefaultAsync(CancellationToken cancellationToken = default)
@ -70,26 +66,23 @@ public class UserIdentityService
if (false == IsAuthenticated()) if (false == IsAuthenticated())
return null; return null;
if (null != _user) if (null != user)
return _user; return user;
if (string.IsNullOrWhiteSpace(Email)) if (string.IsNullOrWhiteSpace(Email))
return null; return null;
// otherwise search for the user by email
var email = Email?.ToLower() ?? "";
//_user = await _dbContext.Users user = await dbContext.Users
// .Include(user => user.SubjectId) .AsNoTracking()
// .FirstOrDefaultAsync(user => user.Email.ToLower() == email, cancellationToken); .FirstOrDefaultAsync(user => user.SubjectId == SubjectId, cancellationToken);
// create user if it doesn't exist // create user if it doesn't exist
if (null == _user) if (null == user)
{ user = await CreateUserAsync(cancellationToken);
_user = await CreateUserAsync(cancellationToken);
return _user; return user;
}
return _user;
} }
public async Task<bool> IsAuthorizedAsync(CancellationToken cancellationToken) public async Task<bool> IsAuthorizedAsync(CancellationToken cancellationToken)
{ {
@ -100,12 +93,17 @@ public class UserIdentityService
{ {
var user = new User var user = new User
{ {
Email = Email,
FirstName = FirstName, FirstName = FirstName,
LastName = LastName, LastName = LastName,
Email = Email,
SubjectId = SubjectId,
Verified = EmailVerified ?? false,
}; };
//await _dbContext.SaveChangesAsync(cancellationToken);
dbContext.Users.Add(user);
await dbContext.SaveChangesAsync(cancellationToken);
return user; return user;
} }

View File

@ -15,6 +15,7 @@
<PackageReference Include="OpenHarbor.Storage.S3" Version="1.1.0" /> <PackageReference Include="OpenHarbor.Storage.S3" Version="1.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Command\User\UpdateUserName\" /> <ProjectReference Include="..\CH.Authority\CH.Authority.csproj" />
<ProjectReference Include="..\CH.Dal\CH.Dal.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.FluentValidation;
namespace CH.CQRS.Command.User;
public static class ServiceCollectionExtension
{
public static IServiceCollection AddUserCommand(this IServiceCollection services)
{
services.AddCommand<UpdateNameCommand, UpdateNameCommandHandler, UpdateNameCommandValidator>();
services.AddCommand<UpdateEmailCommand, UpdateEmailCommandHandler, UpdateEmailCommandValidator>();
return services;
}
}

View File

@ -0,0 +1,46 @@
using CH.CQRS.Service.User;
using CH.CQRS.Service.User.Options;
using CH.Dal;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CH.CQRS.Command.User;
public class UpdateEmailCommand
{
public required string Email { get; set; }
}
public class UpdateEmailCommandHandler(UserService userService) : ICommandHandler<UpdateEmailCommand>
{
public Task HandleAsync(UpdateEmailCommand command, CancellationToken cancellationToken = default)
{
return userService.UpdateEmailAsync(new UpdateEmailCommandOptions
{
Email = command.Email
}, cancellationToken);
}
}
public class UpdateEmailCommandValidator : AbstractValidator<UpdateEmailCommand>
{
public UpdateEmailCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.Email)
.NotEmpty()
.EmailAddress()
.MustAsync(async (email, CancellationToken) =>
{
var emailComparaison = email.Trim().ToLower();
var emailInUse = await dbContext.Users.AnyAsync(user => user.Email.ToLower() == emailComparaison);
return false == emailInUse;
})
.WithMessage("This email is already in use by another user.");
}
}

View File

@ -0,0 +1,35 @@
using CH.CQRS.Service.User;
using CH.CQRS.Service.User.Options;
using CH.Dal;
using FluentValidation;
using OpenHarbor.CQRS.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CH.CQRS.Command.User;
public class UpdateNameCommand
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
public class UpdateNameCommandHandler(UserService userService) : ICommandHandler<UpdateNameCommand>
{
public Task HandleAsync(UpdateNameCommand command, CancellationToken cancellationToken = default)
{
return userService.UpdateNameAsync(new UpdateNameCommandOptions
{
FirstName = command.FirstName,
LastName = command.LastName,
}, cancellationToken);
}
}
public class UpdateNameCommandValidator : AbstractValidator<UpdateNameCommand>
{
public UpdateNameCommandValidator()
{
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using CH.CQRS.Command.User;
using Microsoft.Extensions.DependencyInjection;
using PoweredSoft.Module.Abstractions; using PoweredSoft.Module.Abstractions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -11,7 +12,7 @@ public class CommandModule : IModule
{ {
public IServiceCollection ConfigureServices(IServiceCollection services) public IServiceCollection ConfigureServices(IServiceCollection services)
{ {
services.AddUserCommand();
return services; return services;
} }

View File

@ -0,0 +1,51 @@
using OpenHarbor.CQRS.Abstractions;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using CH.Dal;
namespace CH.CQRS.Query.Health;
public class HealthQuery
{
}
public class HealthQueryHandler(CHDbContext dbContext, IMemoryCache memoryCache) : IQueryHandler<HealthQuery, HealthQueryResult>
{
private const string HealthCheckCacheKey = "HealthStatus";
private const int CacheDurationInSeconds = 60;
public async Task<HealthQueryResult> HandleAsync(HealthQuery query, CancellationToken cancellationToken = new CancellationToken())
{
if (memoryCache.TryGetValue(HealthCheckCacheKey, out var memory))
{
return (HealthQueryResult)memory;
}
var (databaseStatus, databaseLatency) = await MeasureLatencyAsync(CheckDatabaseHealthAsync);
var healthCheckResult = new HealthQueryResult
{
ApiStatus = true,
DatabaseStatus = databaseStatus,
DatabaseLatency = $"{databaseLatency}ms",
};
memoryCache.Set(HealthCheckCacheKey, healthCheckResult, TimeSpan.FromSeconds(CacheDurationInSeconds));
return healthCheckResult;
}
public static async Task<(bool isHealthy, long latencyMs)> MeasureLatencyAsync(Func<Task<bool>> healthCheckAction)
{
var stopwatch = Stopwatch.StartNew();
var isHealthy = await healthCheckAction();
stopwatch.Stop();
return (isHealthy, stopwatch.ElapsedMilliseconds);
}
private async Task<bool> CheckDatabaseHealthAsync()
{
try
{
await dbContext.Database.ExecuteSqlRawAsync("SELECT 1");
return true;
}
catch
{
return false;
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CH.CQRS.Query.Health;
public class HealthQueryResult
{
public bool ApiStatus { get; set; }
public bool DatabaseStatus { get; set; }
public string? DatabaseLatency { get; set; }
}

View File

@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using CH.CQRS.Query.Health;
using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions;
using PoweredSoft.Module.Abstractions; using PoweredSoft.Module.Abstractions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -11,7 +13,7 @@ public class QueryModule : IModule
{ {
public IServiceCollection ConfigureServices(IServiceCollection services) public IServiceCollection ConfigureServices(IServiceCollection services)
{ {
services.AddQuery<HealthQuery, HealthQueryResult, HealthQueryHandler>();
return services; return services;
} }
} }

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CH.CQRS.Service.User.Options;
public class UpdateEmailCommandOptions
{
public required string Email { get; set; }
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CH.CQRS.Service.User.Options;
public class UpdateNameCommandOptions
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}

View File

@ -0,0 +1,44 @@
using CH.Authority.Services;
using CH.CQRS.Service.User.Options;
using CH.Dal;
using Microsoft.EntityFrameworkCore;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
namespace CH.CQRS.Service.User;
public class UserService(CHDbContext dbContext, UserIdentityService identityService)
{
public async Task UpdateEmailAsync(UpdateEmailCommandOptions options,CancellationToken cancellationToken)
{
// todo: change on oauth server first, make sure to persist changes if db save fail after keycloak api is succesful
var user = await dbContext.Users.FirstOrDefaultAsync(user => user.SubjectId == identityService.SubjectId, cancellationToken);
if (null != user)
{
if (user.Email != options.Email)
{
user.Email = options.Email;
}
dbContext.Users.Update(user);
await dbContext.SaveChangesAsync(cancellationToken);
}
return;
}
public async Task UpdateNameAsync(UpdateNameCommandOptions options,CancellationToken cancellationToken)
{
var user = await dbContext.Users.FirstOrDefaultAsync( user => user.SubjectId == identityService.SubjectId, cancellationToken);
if(null != user)
{
if (!String.IsNullOrWhiteSpace(options.FirstName) && user.FirstName != options.FirstName)
{
user.FirstName = options.FirstName;
}
if (!String.IsNullOrWhiteSpace(options.LastName) && user.LastName != options.LastName)
{
user.LastName = options.LastName;
}
dbContext.Users.Update(user);
await dbContext.SaveChangesAsync(cancellationToken);
}
return;
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using CH.CQRS.Service.User;
using Microsoft.Extensions.DependencyInjection;
using PoweredSoft.Module.Abstractions; using PoweredSoft.Module.Abstractions;
namespace CH.CQRS; namespace CH.CQRS;
@ -6,6 +7,7 @@ public class SharedModule : IModule
{ {
public IServiceCollection ConfigureServices(IServiceCollection services) public IServiceCollection ConfigureServices(IServiceCollection services)
{ {
services.AddScoped<UserService>();
return services; return services;
} }
} }

View File

@ -12,8 +12,9 @@ public class CHDbContext(DbContextOptions options) : CHDbScaffoldedContext(optio
{ {
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder);
} }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
return; return;

View File

@ -34,9 +34,13 @@ public partial class CHDbScaffoldedContext : DbContext
entity.ToTable("location"); entity.ToTable("location");
entity.Property(e => e.Id).HasColumnName("id"); entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
.HasColumnName("created_at");
entity.Property(e => e.Name) entity.Property(e => e.Name)
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnName("name"); .HasColumnName("name");
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
}); });
modelBuilder.Entity<Machine>(entity => modelBuilder.Entity<Machine>(entity =>
@ -49,11 +53,15 @@ public partial class CHDbScaffoldedContext : DbContext
entity.Property(e => e.Address) entity.Property(e => e.Address)
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnName("address"); .HasColumnName("address");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
.HasColumnName("created_at");
entity.Property(e => e.LocationId).HasColumnName("location_id"); entity.Property(e => e.LocationId).HasColumnName("location_id");
entity.Property(e => e.Name) entity.Property(e => e.Name)
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnName("name"); .HasColumnName("name");
entity.Property(e => e.Status).HasColumnName("status"); entity.Property(e => e.Status).HasColumnName("status");
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
entity.Property(e => e.UserId).HasColumnName("user_id"); entity.Property(e => e.UserId).HasColumnName("user_id");
entity.HasOne(d => d.Location).WithMany(p => p.Machines) entity.HasOne(d => d.Location).WithMany(p => p.Machines)
@ -67,19 +75,28 @@ public partial class CHDbScaffoldedContext : DbContext
modelBuilder.Entity<User>(entity => modelBuilder.Entity<User>(entity =>
{ {
entity.HasKey(e => e.Id).HasName("user_pkey"); entity.HasKey(e => e.Id).HasName("users_pkey");
entity.ToTable("user"); entity.ToTable("users");
entity.Property(e => e.Id).HasColumnName("id"); entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
.HasColumnName("created_at");
entity.Property(e => e.Email) entity.Property(e => e.Email)
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnName("email"); .HasColumnName("email");
entity.Property(e => e.FirstName).HasMaxLength(255); entity.Property(e => e.FirstName)
entity.Property(e => e.LastName).HasMaxLength(255); .HasMaxLength(255)
.HasColumnName("first_name");
entity.Property(e => e.LastName)
.HasMaxLength(255)
.HasColumnName("last_name");
entity.Property(e => e.SubjectId) entity.Property(e => e.SubjectId)
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnName("subject_id"); .HasColumnName("subject_id");
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
entity.Property(e => e.Verified).HasColumnName("verified");
}); });
OnModelCreatingPartial(modelBuilder); OnModelCreatingPartial(modelBuilder);

View File

@ -8,6 +8,7 @@ public class DalModule : IModule
{ {
public IServiceCollection ConfigureServices(IServiceCollection services) public IServiceCollection ConfigureServices(IServiceCollection services)
{ {
return services; return services.AddSingleton<IAsyncQueryableHandlerService, InMemoryQueryableHandlerService>()
.AddTransient(typeof(IQueryableProvider<>), typeof(DefaultQueryableProvider<>));
} }
} }

View File

@ -9,5 +9,9 @@ public partial class Location
public string Name { get; set; } = null!; public string Name { get; set; } = null!;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public virtual ICollection<Machine> Machines { get; set; } = new List<Machine>(); public virtual ICollection<Machine> Machines { get; set; } = new List<Machine>();
} }

View File

@ -17,6 +17,10 @@ public partial class Machine
public long? UserId { get; set; } public long? UserId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public virtual Location? Location { get; set; } public virtual Location? Location { get; set; }
public virtual User? User { get; set; } public virtual User? User { get; set; }

View File

@ -15,5 +15,11 @@ public partial class User
public string SubjectId { get; set; } = null!; public string SubjectId { get; set; } = null!;
public bool Verified { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public virtual ICollection<Machine> Machines { get; set; } = new List<Machine>(); public virtual ICollection<Machine> Machines { get; set; } = new List<Machine>();
} }

View File

@ -0,0 +1,19 @@
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CH.Dal;
public class DefaultQueryableProvider<TEntity>(CHDbContext dbContext, IServiceProvider serviceProvider) : IQueryableProvider<TEntity>
where TEntity : class
{
public Task<IQueryable<TEntity>> GetQueryableAsync(object query, CancellationToken cancellationToken = default)
{
if (serviceProvider.GetService(typeof(IQueryableProviderOverride<TEntity>)) is IQueryableProviderOverride<TEntity> queryableProviderOverride)
return queryableProviderOverride.GetQueryableAsync(query, cancellationToken);
return Task.FromResult(dbContext.Set<TEntity>().AsQueryable());
}
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CH.Dal;
public interface IQueryableProviderOverride<T>
{
Task<IQueryable<T>> GetQueryableAsync(object query, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,52 @@
using PoweredSoft.Data.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace CH.Dal;
public class InMemoryQueryableHandlerService : IAsyncQueryableHandlerService
{
public Task<bool> AnyAsync<T>(IQueryable<T> queryable, Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default)
{
return Task.FromResult(queryable.Any(predicate));
}
public Task<bool> AnyAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
{
return Task.FromResult(queryable.Any());
}
public bool CanHandle<T>(IQueryable<T> queryable)
{
var result = queryable is EnumerableQuery<T>;
return result;
}
public Task<int> CountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
{
return Task.FromResult(queryable.Count());
}
public Task<T> FirstOrDefaultAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
{
return Task.FromResult(queryable.FirstOrDefault());
}
public Task<T> FirstOrDefaultAsync<T>(IQueryable<T> queryable, Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default)
{
return Task.FromResult(queryable.FirstOrDefault(predicate));
}
public Task<long> LongCountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
{
return Task.FromResult(queryable.LongCount());
}
public Task<List<T>> ToListAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
{
return Task.FromResult(queryable.ToList());
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CH.Dal;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddQueryableProviderOverride<T, TService>(this IServiceCollection services)
where TService : class, IQueryableProviderOverride<T>
{
return services.AddTransient<IQueryableProviderOverride<T>, TService>();
}
}