diff --git a/CH.Api/AppModule.cs b/CH.Api/AppModule.cs index 2c3602d..d4c9d71 100644 --- a/CH.Api/AppModule.cs +++ b/CH.Api/AppModule.cs @@ -9,7 +9,7 @@ public class AppModule : IModule { public IServiceCollection ConfigureServices(IServiceCollection services) { - //services.AddModule(); + services.AddModule(); services.AddModule(); services.AddModule(); services.AddModule(); diff --git a/CH.Api/Program.cs b/CH.Api/Program.cs index f5c0aa3..ea8462f 100644 --- a/CH.Api/Program.cs +++ b/CH.Api/Program.cs @@ -39,14 +39,16 @@ builder.Services.AddDefaultCommandDiscovery(); builder.Services.AddDefaultQueryDiscovery(); builder.Services.AddFluentValidation(); builder.Services.AddModule(); + builder.Services.AddDefaultCommandDiscovery(); builder.Services.AddDefaultQueryDiscovery(); + if (builder.Configuration.GetValue("Swagger:Enable")) { builder.Services.AddEndpointsApiExplorer(); 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 { @@ -105,6 +107,7 @@ mvcBuilder mvcBuilder .AddOpenHarborQueries() .AddOpenHarborDynamicQueries(); + var connectionString = builder.Configuration.GetSection("Database").GetValue("ConnectionString"); var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); @@ -112,13 +115,7 @@ var dataSource = dataSourceBuilder.Build(); builder.Services.AddDbContextPool(options => { - options.UseNpgsql(dataSource, o => - { - // todo: ef 9.0+ - // o.MapEnum("route_file_type"); - }); - - + options.UseNpgsql(dataSource); if (builder.Configuration.GetValue("Debug")) { AppContext.SetSwitch("Npgsql.EnableConnectionStringLogging", true); diff --git a/CH.Api/Properties/launchSettings.json b/CH.Api/Properties/launchSettings.json index c7f15e0..05ba743 100644 --- a/CH.Api/Properties/launchSettings.json +++ b/CH.Api/Properties/launchSettings.json @@ -21,7 +21,7 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "applicationUrl": "https://localhost:7184;http://localhost:5274", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/CH.Authority/Services/UserIdentityService.cs b/CH.Authority/Services/UserIdentityService.cs index 91c7649..c7b9b6e 100644 --- a/CH.Authority/Services/UserIdentityService.cs +++ b/CH.Authority/Services/UserIdentityService.cs @@ -1,22 +1,18 @@ using CH.Dal; using CH.Dal.DbEntity; using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; namespace CH.Authority.Services; public class UserIdentityService { - private User? _user; + private User? user; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IConfiguration _configuration; - private readonly CHDbContext _dbContext; + private readonly IHttpContextAccessor httpContextAccessor; + private readonly IConfiguration configuration; + private readonly CHDbContext dbContext; public string? SubjectId { get; } public string? Email { get; } @@ -28,9 +24,9 @@ public class UserIdentityService public UserIdentityService(CHDbContext dbContext, IHttpContextAccessor httpContextAccessor, IConfiguration configuration) { - _dbContext = dbContext; - _httpContextAccessor = httpContextAccessor; - _configuration = configuration; + this.dbContext = dbContext; + this.httpContextAccessor = httpContextAccessor; + this.configuration = configuration; // microsoft you twats! NameIdentifier = Sub SubjectId = ResolveClaim(ClaimTypes.NameIdentifier); @@ -48,7 +44,7 @@ public class UserIdentityService public bool IsAuthenticated() { - var isAuthenticated = _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false; + var isAuthenticated = httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false; if (!isAuthenticated || null == EmailVerified) return false; @@ -58,11 +54,11 @@ public class UserIdentityService 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) 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; } public async Task GetUserOrDefaultAsync(CancellationToken cancellationToken = default) @@ -70,26 +66,23 @@ public class UserIdentityService if (false == IsAuthenticated()) return null; - if (null != _user) - return _user; + if (null != user) + return user; if (string.IsNullOrWhiteSpace(Email)) return null; - // otherwise search for the user by email - var email = Email?.ToLower() ?? ""; - //_user = await _dbContext.Users - // .Include(user => user.SubjectId) - // .FirstOrDefaultAsync(user => user.Email.ToLower() == email, cancellationToken); + user = await dbContext.Users + .AsNoTracking() + .FirstOrDefaultAsync(user => user.SubjectId == SubjectId, cancellationToken); + // create user if it doesn't exist - if (null == _user) - { - _user = await CreateUserAsync(cancellationToken); - return _user; - } - return _user; + if (null == user) + user = await CreateUserAsync(cancellationToken); + + return user; } public async Task IsAuthorizedAsync(CancellationToken cancellationToken) { @@ -100,12 +93,17 @@ public class UserIdentityService { var user = new User { - Email = Email, FirstName = FirstName, LastName = LastName, + Email = Email, + SubjectId = SubjectId, + Verified = EmailVerified ?? false, }; - //await _dbContext.SaveChangesAsync(cancellationToken); + + + dbContext.Users.Add(user); + await dbContext.SaveChangesAsync(cancellationToken); return user; } diff --git a/CH.CQRS/CH.CQRS.csproj b/CH.CQRS/CH.CQRS.csproj index 385d4fd..86fb279 100644 --- a/CH.CQRS/CH.CQRS.csproj +++ b/CH.CQRS/CH.CQRS.csproj @@ -15,6 +15,7 @@ - + + diff --git a/CH.CQRS/Command/User/ServiceCollectionExtension.cs b/CH.CQRS/Command/User/ServiceCollectionExtension.cs new file mode 100644 index 0000000..dab047f --- /dev/null +++ b/CH.CQRS/Command/User/ServiceCollectionExtension.cs @@ -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(); + services.AddCommand(); + return services; + } +} diff --git a/CH.CQRS/Command/User/UpdateEmailCommand.cs b/CH.CQRS/Command/User/UpdateEmailCommand.cs new file mode 100644 index 0000000..6460ee5 --- /dev/null +++ b/CH.CQRS/Command/User/UpdateEmailCommand.cs @@ -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 +{ + public Task HandleAsync(UpdateEmailCommand command, CancellationToken cancellationToken = default) + { + return userService.UpdateEmailAsync(new UpdateEmailCommandOptions + { + Email = command.Email + }, cancellationToken); + } +} + +public class UpdateEmailCommandValidator : AbstractValidator +{ + 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."); + } +} \ No newline at end of file diff --git a/CH.CQRS/Command/User/UpdateNameCommand.cs b/CH.CQRS/Command/User/UpdateNameCommand.cs new file mode 100644 index 0000000..26643b6 --- /dev/null +++ b/CH.CQRS/Command/User/UpdateNameCommand.cs @@ -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 +{ + public Task HandleAsync(UpdateNameCommand command, CancellationToken cancellationToken = default) + { + return userService.UpdateNameAsync(new UpdateNameCommandOptions + { + FirstName = command.FirstName, + LastName = command.LastName, + }, cancellationToken); + } +} +public class UpdateNameCommandValidator : AbstractValidator +{ + public UpdateNameCommandValidator() + { + } +} diff --git a/CH.CQRS/CommandModule.cs b/CH.CQRS/CommandModule.cs index 7999560..cbc143d 100644 --- a/CH.CQRS/CommandModule.cs +++ b/CH.CQRS/CommandModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using CH.CQRS.Command.User; +using Microsoft.Extensions.DependencyInjection; using PoweredSoft.Module.Abstractions; using System; using System.Collections.Generic; @@ -11,7 +12,7 @@ public class CommandModule : IModule { public IServiceCollection ConfigureServices(IServiceCollection services) { - + services.AddUserCommand(); return services; } diff --git a/CH.CQRS/Query/Health/HealthQuery.cs b/CH.CQRS/Query/Health/HealthQuery.cs new file mode 100644 index 0000000..0840914 --- /dev/null +++ b/CH.CQRS/Query/Health/HealthQuery.cs @@ -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 +{ + private const string HealthCheckCacheKey = "HealthStatus"; + private const int CacheDurationInSeconds = 60; + public async Task 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> healthCheckAction) + { + var stopwatch = Stopwatch.StartNew(); + var isHealthy = await healthCheckAction(); + stopwatch.Stop(); + return (isHealthy, stopwatch.ElapsedMilliseconds); + } + private async Task CheckDatabaseHealthAsync() + { + try + { + await dbContext.Database.ExecuteSqlRawAsync("SELECT 1"); + return true; + } + catch + { + return false; + } + } +} \ No newline at end of file diff --git a/CH.CQRS/Query/Health/HealthQueryResult.cs b/CH.CQRS/Query/Health/HealthQueryResult.cs new file mode 100644 index 0000000..856e250 --- /dev/null +++ b/CH.CQRS/Query/Health/HealthQueryResult.cs @@ -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; } +} diff --git a/CH.CQRS/QueryModule.cs b/CH.CQRS/QueryModule.cs index d4d260e..e6ec018 100644 --- a/CH.CQRS/QueryModule.cs +++ b/CH.CQRS/QueryModule.cs @@ -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 System; using System.Collections.Generic; @@ -11,7 +13,7 @@ public class QueryModule : IModule { public IServiceCollection ConfigureServices(IServiceCollection services) { - + services.AddQuery(); return services; } } diff --git a/CH.CQRS/Service/User/Options/UpdateEmailCommandOptions.cs b/CH.CQRS/Service/User/Options/UpdateEmailCommandOptions.cs new file mode 100644 index 0000000..eecbc67 --- /dev/null +++ b/CH.CQRS/Service/User/Options/UpdateEmailCommandOptions.cs @@ -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; } +} diff --git a/CH.CQRS/Service/User/Options/UpdateNameCommandOptions.cs b/CH.CQRS/Service/User/Options/UpdateNameCommandOptions.cs new file mode 100644 index 0000000..db29918 --- /dev/null +++ b/CH.CQRS/Service/User/Options/UpdateNameCommandOptions.cs @@ -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; } +} diff --git a/CH.CQRS/Service/User/UserService.cs b/CH.CQRS/Service/User/UserService.cs new file mode 100644 index 0000000..cf74377 --- /dev/null +++ b/CH.CQRS/Service/User/UserService.cs @@ -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; + } +} diff --git a/CH.CQRS/SharedModule.cs b/CH.CQRS/SharedModule.cs index 2087fcc..c2f4909 100644 --- a/CH.CQRS/SharedModule.cs +++ b/CH.CQRS/SharedModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using CH.CQRS.Service.User; +using Microsoft.Extensions.DependencyInjection; using PoweredSoft.Module.Abstractions; namespace CH.CQRS; @@ -6,6 +7,7 @@ public class SharedModule : IModule { public IServiceCollection ConfigureServices(IServiceCollection services) { + services.AddScoped(); return services; } } diff --git a/CH.Dal/CHDbContext.cs b/CH.Dal/CHDbContext.cs index b7762a9..1cbec35 100644 --- a/CH.Dal/CHDbContext.cs +++ b/CH.Dal/CHDbContext.cs @@ -12,8 +12,9 @@ public class CHDbContext(DbContextOptions options) : CHDbScaffoldedContext(optio { protected override void OnModelCreating(ModelBuilder modelBuilder) { - + base.OnModelCreating(modelBuilder); } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { return; diff --git a/CH.Dal/CHDbScaffoldedContext.cs b/CH.Dal/CHDbScaffoldedContext.cs index 3df51d1..32bf991 100644 --- a/CH.Dal/CHDbScaffoldedContext.cs +++ b/CH.Dal/CHDbScaffoldedContext.cs @@ -34,9 +34,13 @@ public partial class CHDbScaffoldedContext : DbContext entity.ToTable("location"); 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) .HasMaxLength(255) .HasColumnName("name"); + entity.Property(e => e.UpdatedAt).HasColumnName("updated_at"); }); modelBuilder.Entity(entity => @@ -49,11 +53,15 @@ public partial class CHDbScaffoldedContext : DbContext entity.Property(e => e.Address) .HasMaxLength(255) .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.Name) .HasMaxLength(255) .HasColumnName("name"); entity.Property(e => e.Status).HasColumnName("status"); + entity.Property(e => e.UpdatedAt).HasColumnName("updated_at"); entity.Property(e => e.UserId).HasColumnName("user_id"); entity.HasOne(d => d.Location).WithMany(p => p.Machines) @@ -67,19 +75,28 @@ public partial class CHDbScaffoldedContext : DbContext modelBuilder.Entity(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.CreatedAt) + .HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)") + .HasColumnName("created_at"); entity.Property(e => e.Email) .HasMaxLength(255) .HasColumnName("email"); - entity.Property(e => e.FirstName).HasMaxLength(255); - entity.Property(e => e.LastName).HasMaxLength(255); + entity.Property(e => e.FirstName) + .HasMaxLength(255) + .HasColumnName("first_name"); + entity.Property(e => e.LastName) + .HasMaxLength(255) + .HasColumnName("last_name"); entity.Property(e => e.SubjectId) .HasMaxLength(255) .HasColumnName("subject_id"); + entity.Property(e => e.UpdatedAt).HasColumnName("updated_at"); + entity.Property(e => e.Verified).HasColumnName("verified"); }); OnModelCreatingPartial(modelBuilder); diff --git a/CH.Dal/DalModule.cs b/CH.Dal/DalModule.cs index 901d0f4..301670c 100644 --- a/CH.Dal/DalModule.cs +++ b/CH.Dal/DalModule.cs @@ -8,6 +8,7 @@ public class DalModule : IModule { public IServiceCollection ConfigureServices(IServiceCollection services) { - return services; + return services.AddSingleton() + .AddTransient(typeof(IQueryableProvider<>), typeof(DefaultQueryableProvider<>)); } } \ No newline at end of file diff --git a/CH.Dal/DbEntity/Location.cs b/CH.Dal/DbEntity/Location.cs index c0c1382..4733fdd 100644 --- a/CH.Dal/DbEntity/Location.cs +++ b/CH.Dal/DbEntity/Location.cs @@ -9,5 +9,9 @@ public partial class Location public string Name { get; set; } = null!; + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + public virtual ICollection Machines { get; set; } = new List(); } diff --git a/CH.Dal/DbEntity/Machine.cs b/CH.Dal/DbEntity/Machine.cs index 3078ec8..b4d8000 100644 --- a/CH.Dal/DbEntity/Machine.cs +++ b/CH.Dal/DbEntity/Machine.cs @@ -17,6 +17,10 @@ public partial class Machine public long? UserId { get; set; } + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + public virtual Location? Location { get; set; } public virtual User? User { get; set; } diff --git a/CH.Dal/DbEntity/User.cs b/CH.Dal/DbEntity/User.cs index c7b2e0a..4758867 100644 --- a/CH.Dal/DbEntity/User.cs +++ b/CH.Dal/DbEntity/User.cs @@ -15,5 +15,11 @@ public partial class User public string SubjectId { get; set; } = null!; + public bool Verified { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + public virtual ICollection Machines { get; set; } = new List(); } diff --git a/CH.Dal/DefaultQueryableProvider.cs b/CH.Dal/DefaultQueryableProvider.cs new file mode 100644 index 0000000..919499c --- /dev/null +++ b/CH.Dal/DefaultQueryableProvider.cs @@ -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(CHDbContext dbContext, IServiceProvider serviceProvider) : IQueryableProvider + where TEntity : class +{ + public Task> GetQueryableAsync(object query, CancellationToken cancellationToken = default) + { + if (serviceProvider.GetService(typeof(IQueryableProviderOverride)) is IQueryableProviderOverride queryableProviderOverride) + return queryableProviderOverride.GetQueryableAsync(query, cancellationToken); + + return Task.FromResult(dbContext.Set().AsQueryable()); + } +} \ No newline at end of file diff --git a/CH.Dal/IQueryableProviderOverride.cs b/CH.Dal/IQueryableProviderOverride.cs new file mode 100644 index 0000000..7aaf56f --- /dev/null +++ b/CH.Dal/IQueryableProviderOverride.cs @@ -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 +{ + Task> GetQueryableAsync(object query, CancellationToken cancellationToken = default); +} diff --git a/CH.Dal/InMemoryQueryableHandlerService.cs b/CH.Dal/InMemoryQueryableHandlerService.cs new file mode 100644 index 0000000..8bcf642 --- /dev/null +++ b/CH.Dal/InMemoryQueryableHandlerService.cs @@ -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 AnyAsync(IQueryable queryable, Expression> predicate, CancellationToken cancellationToken = default) + { + return Task.FromResult(queryable.Any(predicate)); + } + + public Task AnyAsync(IQueryable queryable, CancellationToken cancellationToken = default) + { + return Task.FromResult(queryable.Any()); + } + + public bool CanHandle(IQueryable queryable) + { + var result = queryable is EnumerableQuery; + return result; + } + + public Task CountAsync(IQueryable queryable, CancellationToken cancellationToken = default) + { + return Task.FromResult(queryable.Count()); + } + + public Task FirstOrDefaultAsync(IQueryable queryable, CancellationToken cancellationToken = default) + { + return Task.FromResult(queryable.FirstOrDefault()); + } + + public Task FirstOrDefaultAsync(IQueryable queryable, Expression> predicate, CancellationToken cancellationToken = default) + { + return Task.FromResult(queryable.FirstOrDefault(predicate)); + } + + public Task LongCountAsync(IQueryable queryable, CancellationToken cancellationToken = default) + { + return Task.FromResult(queryable.LongCount()); + } + + public Task> ToListAsync(IQueryable queryable, CancellationToken cancellationToken = default) + { + return Task.FromResult(queryable.ToList()); + } +} diff --git a/CH.Dal/ServiceCollectionExtensions.cs b/CH.Dal/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..2128381 --- /dev/null +++ b/CH.Dal/ServiceCollectionExtensions.cs @@ -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(this IServiceCollection services) + where TService : class, IQueryableProviderOverride + { + return services.AddTransient, TService>(); + } +}