add validations to command

This commit is contained in:
DavidGudEnough 2025-01-13 19:19:03 -05:00
parent 8eefa30d52
commit 1451314d56
Signed by: david.nguyen
GPG Key ID: 0B95DC36355BEB37
30 changed files with 334 additions and 117 deletions

View File

@ -13,6 +13,7 @@ namespace CH.CQRS.Command.CryptoStats;
public class GetCryptoStatsCommand
{
}
public class GetCryptoStatsCommandHandler(CryptoService cryptoService) : ICommandHandler<GetCryptoStatsCommand>
{
public Task HandleAsync(GetCryptoStatsCommand command, CancellationToken cancellationToken = default)
@ -20,6 +21,7 @@ public class GetCryptoStatsCommandHandler(CryptoService cryptoService) : IComman
return cryptoService.GetCryptoStatsAsync(cancellationToken);
}
}
public class GetCryptoStatsCommandValidator : AbstractValidator<GetCryptoStatsCommand>
{
public GetCryptoStatsCommandValidator()

View File

@ -1,6 +1,10 @@
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS.Abstractions;
namespace CH.CQRS.Command.Energy;
@ -24,8 +28,16 @@ public class CreateEnergyProviderCommandHandler(EnergyService energyService) : I
public class CreateEnergyProviderCommandValidator : AbstractValidator<CreateEnergyProviderCommand>
{
public CreateEnergyProviderCommandValidator()
public CreateEnergyProviderCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.Name)
.NotEmpty()
.MinimumLength(3)
.MustAsync(async (name, cancellationToken) =>
{
var nameInUse = await dbContext.EnergyProviders.AnyAsync(energyProvider => energyProvider.Name == name, cancellationToken);
return false == nameInUse;
})
.WithMessage("This Name is already in use by another energy provider.");
}
}

View File

@ -1,6 +1,11 @@
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using CH.Enum;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS.Abstractions;
namespace CH.CQRS.Command.Energy;
@ -10,7 +15,7 @@ public class CreateEnergyRateCommand
public long ProviderId { get; set; }
public required string Name { get; set; }
public decimal Price { get; set; }
public required string? Currency { get; set; }
public Currency Currency { get; set; }
public bool Active { get; set; }
}
public class CreateEnergyRateCommandHandler(EnergyService energyService) : ICommandHandler<CreateEnergyRateCommand>
@ -30,10 +35,21 @@ public class CreateEnergyRateCommandHandler(EnergyService energyService) : IComm
public class CreateEnergyRateCommandValidator : AbstractValidator<CreateEnergyRateCommand>
{
public CreateEnergyRateCommandValidator()
public CreateEnergyRateCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.Name)
.NotEmpty()
.MinimumLength(3)
.MustAsync(async (name, cancellationToken) =>
{
var nameInUse = await dbContext.EnergyRates.AnyAsync(energyRate => energyRate.Name == name, cancellationToken);
return false == nameInUse;
})
.WithMessage("This Name is already in use by another energy rate.");
RuleFor(command => command.Price).GreaterThanOrEqualTo(0);
RuleFor(command => command.ProviderId)
.NotEmpty();
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyProvider,long>(dbContext))
.WithMessage("The provided provider Id is invalid.");
}
}

View File

@ -1,6 +1,11 @@
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using CH.Enum;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS.Abstractions;
namespace CH.CQRS.Command.Energy;
@ -10,7 +15,7 @@ public class CreateEnergyRateExceptionCommand
public long RateId { get; set; }
public required string Name { get; set; }
public decimal EnergyThreshold { get; set; }
public string? ResetType { get; set; }
public EnergyRateExceptionThresholdResetType ResetType { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? EndedAt { get; set; }
}
@ -33,11 +38,24 @@ public class CreateEnergyRateExceptionCommandHandler(EnergyService energyService
}
public class CreateEnergyRateExceptionCommandValidator : AbstractValidator<CreateEnergyRateExceptionCommand>
{
public CreateEnergyRateExceptionCommandValidator()
public CreateEnergyRateExceptionCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.Name)
.NotEmpty()
.MinimumLength(3)
.MustAsync(async (name, cancellationToken) =>
{
var nameInUse = await dbContext.EnergyRateExceptions.AnyAsync(energyRateException => energyRateException.Name == name, cancellationToken);
return false == nameInUse;
})
.WithMessage("This Name is already in use by another energy rate exception.");
RuleFor(command => command.EnergyThreshold)
.GreaterThan(0);
RuleFor(command => command.RateId)
.NotEmpty();
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyRate,long>(dbContext))
.WithMessage("The provided Rate Id is invalid.");
}
}

View File

@ -1,5 +1,8 @@
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using FluentValidation;
using OpenHarbor.CQRS.Abstractions;
@ -24,9 +27,12 @@ public class DisableEnergyProviderCommandHandler(EnergyService energyService) :
public class DisableEnergyProviderCommandValidator : AbstractValidator<DisableEnergyProviderCommand>
{
public DisableEnergyProviderCommandValidator()
public DisableEnergyProviderCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.ProviderId)
.NotEmpty();
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyProvider,long>(dbContext))
.WithMessage("The provided provider Id is invalid.");
}
}

View File

@ -1,5 +1,8 @@
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using FluentValidation;
using OpenHarbor.CQRS.Abstractions;
@ -24,9 +27,11 @@ public class DisableEnergyRateCommandHandler(EnergyService energyService) : ICom
public class DisableEnergyRateCommandValidator : AbstractValidator<DisableEnergyRateCommand>
{
public DisableEnergyRateCommandValidator()
public DisableEnergyRateCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.RateId)
.NotEmpty();
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyRate,long>(dbContext))
.WithMessage("The provided Rate Id is invalid.");
}
}

View File

@ -1,5 +1,8 @@
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using FluentValidation;
using OpenHarbor.CQRS.Abstractions;
@ -22,8 +25,12 @@ public class EnableEnergyProviderCommandHandler(EnergyService energyService) : I
public class EnableEnergyProviderCommandValidator : AbstractValidator<EnableEnergyProviderCommand>
{
public EnableEnergyProviderCommandValidator()
public EnableEnergyProviderCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.ProviderId)
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyProvider,long>(dbContext))
.WithMessage("The provided provider Id is invalid.");
}
}

View File

@ -1,4 +1,8 @@
using CH.CQRS.Service.Energy;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using FluentValidation;
using OpenHarbor.CQRS.Abstractions;
namespace CH.CQRS.Command.Energy;
@ -17,3 +21,14 @@ public class EnableEnergyRateCommandHandler(EnergyService energyService) : IComm
}, cancellationToken);
}
}
public class EnableEnergyRateCommandValidator : AbstractValidator<EnableEnergyRateCommand>
{
public EnableEnergyRateCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.RateId)
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyRate,long>(dbContext))
.WithMessage("The provided Rate Id is invalid.");
}
}

View File

@ -10,18 +10,24 @@ public static class ServiceCollectionExtension
services
.AddCommand<CreateEnergyProviderCommand, CreateEnergyProviderCommandHandler,
CreateEnergyProviderCommandValidator>();
services
.AddCommand<CreateEnergyRateExceptionCommand, CreateEnergyRateExceptionCommandHandler,
CreateEnergyRateExceptionCommandValidator>();
services
.AddCommand<CreateEnergyRateCommand, CreateEnergyRateCommandHandler,
CreateEnergyRateCommandValidator>();
services
.AddCommand<CreateEnergyRateExceptionCommand, CreateEnergyRateExceptionCommandHandler,
CreateEnergyRateExceptionCommandValidator>();
services
.AddCommand<DisableEnergyProviderCommand, DisableEnergyProviderCommandHandler,
DisableEnergyProviderCommandValidator>();
services
.AddCommand<DisableEnergyRateCommand, DisableEnergyRateCommandHandler,
DisableEnergyRateCommandValidator>();
services
.AddCommand<EnableEnergyProviderCommand, EnableEnergyProviderCommandHandler,
EnableEnergyProviderCommandValidator>();
services
.AddCommand<EnableEnergyRateCommand, EnableEnergyRateCommandHandler,
EnableEnergyRateCommandValidator>();
services
.AddCommand<UpdateEnergyProviderCommand, UpdateEnergyProviderCommandHandler,
UpdateEnergyProviderCommandValidator>();

View File

@ -1,6 +1,10 @@
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS.Abstractions;
namespace CH.CQRS.Command.Energy;
@ -24,9 +28,20 @@ public class UpdateEnergyProviderCommandHandler(EnergyService energyService) : I
}
public class UpdateEnergyProviderCommandValidator : AbstractValidator<UpdateEnergyProviderCommand>
{
public UpdateEnergyProviderCommandValidator()
public UpdateEnergyProviderCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.Name)
.NotEmpty()
.MinimumLength(3)
.MustAsync(async (name, cancellationToken) =>
{
var nameInUse = await dbContext.EnergyProviders.AnyAsync(energyProvider => energyProvider.Name == name, cancellationToken);
return false == nameInUse;
})
.WithMessage("This Name is already in use by another energy provider.");
RuleFor(command => command.ProviderId)
.NotEmpty();
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyProvider,long>(dbContext));
}
}

View File

@ -1,6 +1,11 @@
using System.Data;
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS.Abstractions;
namespace CH.CQRS.Command.Energy;
@ -8,11 +13,9 @@ namespace CH.CQRS.Command.Energy;
public class UpdateEnergyRateCommand
{
public long RateId { get; set; }
public bool SendAlert { get; set; }
public required DateTime StartedAt { get; set; }
public DateTime? AppliedAt { get; set; }
public string? Name { get; set; }
public DateTime? StartedAt { get; set; }
public decimal? Rate { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public class UpdateEnergyRateCommandHandler(EnergyService energyService) : ICommandHandler<UpdateEnergyRateCommand>
{
@ -21,19 +24,34 @@ public class UpdateEnergyRateCommandHandler(EnergyService energyService) : IComm
return energyService.UpdateEnergyRateAsync(new UpdateEnergyRateCommandOptions
{
RateId = command.RateId,
Name = command.Name,
StartedAt = command.StartedAt,
AppliedAt = command.AppliedAt,
Rate = command.Rate,
SendAlert = command.SendAlert,
UpdatedAt = command.UpdatedAt
Rate = command.Rate
}, cancellationToken);
}
}
public class UpdateEnergyRateCommandValidator : AbstractValidator<UpdateEnergyRateCommand>
{
public UpdateEnergyRateCommandValidator()
public UpdateEnergyRateCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.Name)
.NotEmpty()
.MinimumLength(3)
.When(command => false == String.IsNullOrWhiteSpace(command.Name))
.MustAsync(async (name, cancellationToken) =>
{
var nameInUse = await dbContext.EnergyRates.AnyAsync(energyRate => energyRate.Name == name, cancellationToken);
return false == nameInUse;
})
.WithMessage("This Name is already in use by another energy rate.");
RuleFor(command => command.RateId)
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyRate,long>(dbContext))
.WithMessage("The provided Rate Id is invalid.");
RuleFor(command => command.Rate)
.GreaterThanOrEqualTo(0);
}
}

View File

@ -1,6 +1,11 @@
using CH.CQRS.Service.Energy;
using CH.CQRS.Service.Energy.Options;
using CH.Dal;
using CH.Dal.DbEntity;
using CH.Dal.Validators;
using CH.Enum;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS.Abstractions;
namespace CH.CQRS.Command.Energy;
@ -8,8 +13,8 @@ namespace CH.CQRS.Command.Energy;
public class UpdateEnergyRateExceptionCommand
{
public long EnergyRateExceptionId { get; set; }
public decimal EnergyThreshold { get; set; }
public string? ResetType { get; set; }
public decimal? EnergyThreshold { get; set; }
public EnergyRateExceptionThresholdResetType ResetType { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? EndedAt { get; set; }
}
@ -31,11 +36,14 @@ public class UpdateEnergyRateExceptionCommandHandler(EnergyService energyService
public class UpdateEnergyRateExceptionCommandValidator : AbstractValidator<UpdateEnergyRateExceptionCommand>
{
public UpdateEnergyRateExceptionCommandValidator()
public UpdateEnergyRateExceptionCommandValidator(CHDbContext dbContext)
{
RuleFor(command => command.EnergyRateExceptionId)
.NotEmpty();
.NotEmpty()
.SetValidator(new DbEntityExistValidator<EnergyRateException,long>(dbContext));
RuleFor(command => command.EnergyThreshold)
.GreaterThan(0);
.GreaterThan(0)
.When(command => command.EnergyThreshold.HasValue);
}
}

View File

@ -28,7 +28,7 @@ public class EnergyService(CHDbContext dbContext)
var energyRate = new EnergyRate
{
Name = options.Name,
Currency = (Currency)System.Enum.Parse(typeof(Currency), options.Currency?.ToUpper()),
Currency = options.Currency,
Price = options.Price,
CreatedAt = DateTime.UtcNow,
Provider = provider,
@ -51,8 +51,7 @@ public class EnergyService(CHDbContext dbContext)
StartedAt = options.StartedAt,
EndedAt = options.EndedAt,
EnergyThreshold = options.EnergyThreshold,
ResetType = (EnergyRateExceptionThresholdResetType)System.Enum
.Parse(typeof(EnergyRateExceptionThresholdResetType), options.ResetType.ToLower() ?? ""),
ResetType = options.ResetType,
};
await dbContext.AddAsync(energyRateException, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
@ -61,29 +60,54 @@ public class EnergyService(CHDbContext dbContext)
public async Task UpdateEnergyProviderAsync(UpdateEnergyProviderCommandOptions options,
CancellationToken cancellationToken)
{
var provider = await dbContext.EnergyProviders.FirstOrDefaultAsync(provider => provider.Id == options.ProviderId, cancellationToken);
if (null != provider)
{
provider.Name = options.Name;
provider.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken);
}
var provider = await dbContext.EnergyProviders
.AsTracking()
.FirstOrDefaultAsync(provider => provider.Id == options.ProviderId, cancellationToken);
if (provider is null)
return;
provider.Name = options.Name;
provider.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken);
}
public async Task UpdateEnergyRateValueAsync(UpdateEnergyRateCommandOptions options, CancellationToken cancellationToken)
{
var energyRate = await dbContext.EnergyRates
.AsTracking()
.FirstOrDefaultAsync(energyRate => energyRate.Id == options.RateId, cancellationToken);
if (energyRate is null)
return;
if (options.Rate.HasValue)
energyRate.Price = options.Rate.Value;
if (false == string.IsNullOrWhiteSpace(options.Name))
energyRate.Name = options.Name;
energyRate.UpdatedAt = DateTime.UtcNow;
}
public async Task UpdateEnergyRateAsync(UpdateEnergyRateCommandOptions options, CancellationToken cancellationToken)
{
var energyRateUpdate = new EnergyRateUpdate
if (options.StartedAt.HasValue)
{
RateId = options.RateId,
Rate = options.Rate,
StartedAt = options.StartedAt,
CreatedAt = DateTime.UtcNow,
UpdatedAt = options.UpdatedAt,
AppliedAt = options.AppliedAt,
SendAlert = options.SendAlert,
};
await dbContext.AddAsync(energyRateUpdate, cancellationToken);
var energyRateUpdate = new EnergyRateUpdate
{
RateId = options.RateId,
Rate = options.Rate,
Name = options.Name ?? "",
StartedAt = options.StartedAt.Value,
};
await dbContext.AddAsync(energyRateUpdate, cancellationToken);
}
else
{
await UpdateEnergyRateValueAsync(options, cancellationToken);
}
await dbContext.SaveChangesAsync(cancellationToken);
}
@ -91,19 +115,29 @@ public class EnergyService(CHDbContext dbContext)
CancellationToken cancellationToken)
{
var energyRateException = await dbContext.EnergyRateExceptions
.AsTracking()
.FirstOrDefaultAsync(rateException => rateException.Id == options.EnergyRateExceptionId, cancellationToken);
if (null != energyRateException)
{
if (energyRateException is null)
return;
if (options.EnergyThreshold.HasValue)
energyRateException.EnergyThreshold = options.EnergyThreshold;
if (options.StartedAt.HasValue)
energyRateException.StartedAt = options.StartedAt;
if (options.EndedAt.HasValue)
energyRateException.EndedAt = options.EndedAt;
energyRateException.UpdatedAt = DateTime.UtcNow;
if (String.IsNullOrWhiteSpace(options.ResetType))
{
energyRateException.ResetType = (EnergyRateExceptionThresholdResetType)System.Enum.Parse(typeof(EnergyRateExceptionThresholdResetType),options.ResetType) ;
}
await dbContext.SaveChangesAsync(cancellationToken);
energyRateException.UpdatedAt = DateTime.UtcNow;
if (energyRateException.ResetType != options.ResetType)
{
energyRateException.ResetType = options.ResetType;
}
await dbContext.SaveChangesAsync(cancellationToken);
}
public async Task DisableEnergyProviderAsync(DisableEnergyProviderCommandOptions options,
@ -113,7 +147,7 @@ public class EnergyService(CHDbContext dbContext)
if (null != energyProvider)
{
energyProvider.Active = false;
energyProvider.DisabledAt = options.DisabledAt;
energyProvider.DisabledAt = options.DisabledAt ?? DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken);
}
}
@ -124,7 +158,7 @@ public class EnergyService(CHDbContext dbContext)
if (null != energyRate)
{
energyRate.Active = false;
energyRate.DisabledAt = options.DisabledAt;
energyRate.DisabledAt = options.DisabledAt ?? DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken);
}
}

View File

@ -1,4 +1,5 @@
using Amazon.Runtime.Internal.Transform;
using CH.Enum;
namespace CH.CQRS.Service.Energy.Options;
@ -7,6 +8,6 @@ public class CreateEnergyRateCommandOptions
public long ProviderId { get; set; }
public required string Name { get; set; }
public decimal? Price { get; set; }
public required string? Currency { get; set; }
public Currency Currency { get; set; }
public bool Active { get; set; }
}

View File

@ -1,3 +1,5 @@
using CH.Enum;
namespace CH.CQRS.Service.Energy.Options;
public class CreateEnergyRateExceptionCommandOptions
@ -5,7 +7,7 @@ public class CreateEnergyRateExceptionCommandOptions
public long RateId { get; set; }
public required string Name { get; set; }
public decimal EnergyThreshold { get; set; }
public string? ResetType { get; set; }
public EnergyRateExceptionThresholdResetType ResetType { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? EndedAt { get; set; }
}

View File

@ -3,9 +3,7 @@ namespace CH.CQRS.Service.Energy.Options;
public class UpdateEnergyRateCommandOptions
{
public long RateId { get; set; }
public bool SendAlert { get; set; }
public required DateTime StartedAt { get; set; }
public DateTime? AppliedAt { get; set; }
public DateTime? StartedAt { get; set; }
public decimal? Rate { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? Name { get; set; }
}

View File

@ -1,10 +1,12 @@
using CH.Enum;
namespace CH.CQRS.Service.Energy.Options;
public class UpdateEnergyRateExceptionCommandOptions
{
public long EnergyRateExceptionId { get; set; }
public decimal EnergyThreshold { get; set; }
public string? ResetType { get; set; }
public decimal? EnergyThreshold { get; set; }
public EnergyRateExceptionThresholdResetType ResetType { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? EndedAt { get; set; }
}

View File

@ -1,7 +0,0 @@
namespace CH.Dal.Abstractions
{
public class Class1
{
}
}

View File

@ -0,0 +1,6 @@
namespace CH.Dal.Abstractions;
public interface IHasId<T>
{
public T Id { get; set; }
}

View File

@ -18,6 +18,7 @@
<PackageReference Include="PoweredSoft.Module.Abstractions" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CH.Dal.Abstractions\CH.Dal.Abstractions.csproj" />
<ProjectReference Include="..\CH.Enum\CH.Enum.csproj" />
</ItemGroup>

View File

@ -112,15 +112,14 @@ public partial class CHDbScaffoldedContext : DbContext
entity.ToTable("energy_rate_update");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.AppliedAt).HasColumnName("applied_at");
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.Rate).HasColumnName("rate");
entity.Property(e => e.RateId).HasColumnName("rate_id");
entity.Property(e => e.SendAlert)
.HasDefaultValue(false)
.HasColumnName("send_alert");
entity.Property(e => e.StartedAt).HasColumnName("started_at");
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");

View File

@ -0,0 +1,8 @@
using CH.Dal.Abstractions;
namespace CH.Dal.DbEntity;
public partial class EnergyProvider : IHasId<long>
{
}

View File

@ -1,9 +1,10 @@
using System.ComponentModel.DataAnnotations.Schema;
using CH.Dal.Abstractions;
using CH.Enum;
namespace CH.Dal.DbEntity;
public partial class EnergyRate
public partial class EnergyRate : IHasId<long>
{
[Column(TypeName = "currency")]
public Currency Currency { get; set; }

View File

@ -1,9 +1,10 @@
using System.ComponentModel.DataAnnotations.Schema;
using CH.Dal.Abstractions;
using CH.Enum;
namespace CH.Dal.DbEntity;
public partial class EnergyRateException
public partial class EnergyRateException : IHasId<long>
{
[Column(TypeName = "energy_rate_exception_treshold_reset_type")]
[Column(TypeName = "energy_rate_exception_threshold_reset_type")]
public EnergyRateExceptionThresholdResetType ResetType { get; set; }
}

View File

@ -9,14 +9,12 @@ public partial class EnergyRateUpdate
public long? RateId { get; set; }
public bool SendAlert { get; set; }
public string Name { get; set; } = null!;
public DateTime StartedAt { get; set; }
public decimal? Rate { get; set; }
public DateTime? AppliedAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }

View File

@ -0,0 +1,25 @@
using CH.Dal.Abstractions;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
namespace CH.Dal.Validators;
public class DbEntityExistValidator<TEntity, TValue> : AbstractValidator<TValue>
where TEntity : class, IHasId<TValue>
{
private readonly CHDbContext _dbContext;
public DbEntityExistValidator(CHDbContext dbContext)
{
_dbContext = dbContext;
RuleFor(value => value)
.NotNull()
.MustAsync(IsEntityExist)
.WithMessage("Invalid Entity");
}
private Task<bool> IsEntityExist(TValue id,
CancellationToken cancellationToken)
=> _dbContext.Set<TEntity>().AnyAsync(entity => entity.Id!.Equals(id), cancellationToken);
}

View File

@ -1,12 +1,12 @@
using System.Text;
using IdentityModel.Client;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System.Text;
using IdentityModel.Client;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
namespace CH.KeycloakApi;
namespace CH.KeycloakApi;
public class KeycloakService
{
public class KeycloakService
{
private readonly KeycloakSettings settings;
public KeycloakService(IConfiguration configuration)
@ -17,9 +17,10 @@
public KeycloakSettings Settings => settings;
public async Task<string> GetTokenAsync()
// use token manager instead
public async Task<string> GetTokenAsync(CancellationToken cancellationToken = default)
{
var tokenEndpoint = $"{this.settings.Endpoint}/realms/master/protocol/openid-connect/token";
var tokenEndpoint = $"{this.settings.Endpoint}/realms/${settings.ApiRealm}/protocol/openid-connect/token";
var client = new HttpClient();
var response = await client.RequestTokenAsync(new TokenRequest
{
@ -27,12 +28,12 @@
GrantType = "client_credentials",
ClientId = this.settings.ClientId,
ClientSecret = this.settings.ClientSecret,
});
}, cancellationToken);
return response.AccessToken;
}
public async Task<KeycloakUser> GetUserByEmailAsync(string realm, string email)
public async Task<KeycloakUser?> GetUserByEmailAsync(string realm, string email)
{
var httpClient = new HttpClient();
var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users?email={email}";
@ -40,11 +41,11 @@
var response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var ret = JsonConvert.DeserializeObject<List<KeycloakUser>>(json);
return ret.FirstOrDefault();
var keycloakUsers = JsonConvert.DeserializeObject<List<KeycloakUser>>(json);
return keycloakUsers?.FirstOrDefault();
}
public async Task<KeycloakUser> GetUserByIdAsync(string realm, string id)
public async Task<KeycloakUser?> GetUserByIdAsync(string realm, string id)
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync());
@ -52,8 +53,8 @@
var response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var ret = JsonConvert.DeserializeObject<KeycloakUser>(json);
return ret;
var keycloakUser = JsonConvert.DeserializeObject<KeycloakUser>(json);
return keycloakUser;
}
public async Task SendChangePasswordEmailAsync(string realm, string id)
@ -69,7 +70,7 @@
response.EnsureSuccessStatusCode();
}
public async Task<List<KeycloakUser>> GetUsersAsync(string realm, string search = null, int max = 100)
public async Task<List<KeycloakUser>?> GetUsersAsync(string realm, string? search = null, int max = 100)
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync());
@ -79,14 +80,15 @@
var response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var ret = JsonConvert.DeserializeObject<List<KeycloakUser>>(json);
return ret;
var keycloakUsers = JsonConvert.DeserializeObject<List<KeycloakUser>>(json);
return keycloakUsers;
}
public async Task ChangePasswordAsync(string realm, string id, string newPassword, bool temporary)
{
///auth/admin/realms/{realm}/users/{id}/reset-password
// auth/admin/realms/{realm}/users/{id}/reset-password
/////{ "type": "password", "temporary": false, "value": "my-new-password" }
var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users/{id}/reset-password";
var httpClient = new HttpClient();
@ -128,7 +130,7 @@
return await this.GetUserByEmailAsync(realm, email) != null;
}
public async Task<KeycloakUser> CreateOrUpdateAsync(string realm, string email, string firstName, string lastName, bool enabled)
public async Task<KeycloakUser?> CreateOrUpdateAsync(string realm, string email, string firstName, string lastName, bool enabled)
{
var existingUser = await GetUserByEmailAsync(realm, email);
if (existingUser != null)
@ -140,7 +142,7 @@
return await CreateUserAsync(realm, email, firstName, lastName, enabled);
}
public async Task<KeycloakUser> CreateUserAsync(string realm, string email, string firstName, string lastName, bool enabled)
public async Task<KeycloakUser?> CreateUserAsync(string realm, string email, string firstName, string lastName, bool enabled)
{
long epochTicks = new DateTime(1970, 1, 1).Ticks;
long unixTime = ((DateTime.UtcNow.Ticks - epochTicks) / TimeSpan.TicksPerSecond);
@ -174,7 +176,7 @@
var postJson = JsonConvert.SerializeObject(user);
var response = await httpClient.PostAsync(url, new StringContent(postJson, Encoding.UTF8, "application/json"));
response.EnsureSuccessStatusCode();
var ret = await GetUserByEmailAsync(realm, email);
return ret;
}
var keycloakUser = await GetUserByEmailAsync(realm, email);
return keycloakUser;
}
}

View File

@ -5,6 +5,6 @@ public class KeycloakSettings
public string Endpoint { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string EmployeeRealm { get; set; }
public string UserRealm { get; set; }
public string ApiRealm { get; set; }
public string Realm { get; set; }
}

View File

@ -7,28 +7,40 @@ public class KeycloakUser
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("createdTimestamp")]
public long CreatedTimestamp { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("enabled")]
public bool Enabled { get; set; }
[JsonProperty("totp")]
public bool Totp { get; set; }
[JsonProperty("emailVerified")]
public bool EmailVerified { get; set; }
[JsonProperty("firstName")]
public string FirstName { get; set; }
[JsonProperty("lastName")]
public string LastName { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("notBefore")]
public int NotBefore { get; set; }
[JsonProperty("requiredActions")]
public List<string> RequiredActions { get; set; }
[JsonProperty("attributes")]
public JObject Attributes { get; set; }
public JObject? Attributes { get; set; }
[JsonProperty("access")]
public Dictionary<string, object> Access { get; set; }
}

View File

@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CH.Enum", "CH.Enum\CH.Enum.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CH.KeycloakApi", "CH.KeycloakApi\CH.KeycloakApi.csproj", "{28648B65-7D84-460A-993E-0F98D94CECFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CH.Dal.Abstractions", "CH.Dal.Abstractions\CH.Dal.Abstractions.csproj", "{C1584D53-DEC2-42B4-BEC9-3CCEECA88CB6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -75,6 +77,10 @@ Global
{28648B65-7D84-460A-993E-0F98D94CECFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28648B65-7D84-460A-993E-0F98D94CECFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28648B65-7D84-460A-993E-0F98D94CECFB}.Release|Any CPU.Build.0 = Release|Any CPU
{C1584D53-DEC2-42B4-BEC9-3CCEECA88CB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1584D53-DEC2-42B4-BEC9-3CCEECA88CB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1584D53-DEC2-42B4-BEC9-3CCEECA88CB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1584D53-DEC2-42B4-BEC9-3CCEECA88CB6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE