Skip to content

Commit

Permalink
Implementing Repositories & Event Handlers. (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 authored Dec 22, 2024
1 parent bbf05e6 commit 85ddfdd
Show file tree
Hide file tree
Showing 23 changed files with 883 additions and 741 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Logitar.Identity.EntityFrameworkCore.Relational.Entities;
using Logitar.Identity.Core;
using Logitar.Identity.EntityFrameworkCore.Relational.Entities;
using Microsoft.EntityFrameworkCore;

namespace Logitar.Identity.EntityFrameworkCore.Relational.CustomAttributes;
namespace Logitar.Identity.EntityFrameworkCore.Relational;

public class CustomAttributeService : ICustomAttributeService
{
Expand All @@ -20,40 +21,39 @@ public virtual async Task RemoveAsync(string entityType, int entityId, Cancellat
Context.CustomAttributes.RemoveRange(entities);
}

public virtual async Task SynchronizeAsync(string entityType, int entityId, Dictionary<string, string?> customAttributes, CancellationToken cancellationToken)
public virtual async Task UpdateAsync(string entityType, int entityId, IReadOnlyDictionary<Identifier, string?> customAttributes, CancellationToken cancellationToken)
{
if (customAttributes.Count == 0)
if (customAttributes.Count < 1)
{
return;
}

Dictionary<string, CustomAttributeEntity> entities = (await Context.CustomAttributes
.Where(x => x.EntityType == entityType && x.EntityId == entityId)
.ToArrayAsync(cancellationToken)
).ToDictionary(x => x.Key, x => x);
.ToArrayAsync(cancellationToken)).ToDictionary(x => x.Key, x => x);

foreach (KeyValuePair<string, string?> customAttribute in customAttributes)
foreach (KeyValuePair<Identifier, string?> customAttribute in customAttributes)
{
if (customAttribute.Value == null)
{
if (entities.TryGetValue(customAttribute.Key, out CustomAttributeEntity? entity))
if (entities.TryGetValue(customAttribute.Key.Value, out CustomAttributeEntity? entity))
{
Context.CustomAttributes.Remove(entity);
}
}
else
{
if (!entities.TryGetValue(customAttribute.Key, out CustomAttributeEntity? entity))
if (!entities.TryGetValue(customAttribute.Key.Value, out CustomAttributeEntity? entity))
{
entity = new()
{
EntityType = entityType,
EntityId = entityId,
Key = customAttribute.Key
Key = customAttribute.Key.Value
};

Context.CustomAttributes.Add(entity);
entities[customAttribute.Key] = entity;
entities[customAttribute.Key.Value] = entity;
}

entity.Value = customAttribute.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,12 @@ public static IServiceCollection AddLogitarIdentityWithEntityFrameworkCoreRelati
return services
.AddLogitarEventSourcingWithEntityFrameworkCoreRelational()
.AddLogitarIdentityInfrastructure()
.AddEventHandlers()
.AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
.AddRepositories()
//.AddTransient<ICustomAttributeService, CustomAttributeService>() // TODO(fpion): implement
.AddTransient<ICustomAttributeService, CustomAttributeService>()
.AddScoped<ITokenBlacklist, TokenBlacklist>();
}

private static IServiceCollection AddEventHandlers(this IServiceCollection services)
{
return services/*
.AddTransient<IApiKeyEventHandler, ApiKeyEventHandler>()
.AddTransient<IOneTimePasswordEventHandler, OneTimePasswordEventHandler>()
.AddTransient<IRoleEventHandler, RoleEventHandler>()
.AddTransient<ISessionEventHandler, SessionEventHandler>()
.AddTransient<IUserEventHandler, UserEventHandler>()*/; // TODO(fpion): implement
}

private static IServiceCollection AddRepositories(this IServiceCollection services)
{
return services
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ public void SetEmail(UserEmailChanged @event)
}
}

public void RemovePassword(UserPasswordRemoved @event)
{
Update(@event);

PasswordHash = null;
PasswordChangedBy = null;
PasswordChangedOn = null;
}

public void SetPassword(UserPasswordEvent @event)
{
Update(@event);
Expand Down
10 changes: 10 additions & 0 deletions lib/Logitar.Identity.EntityFrameworkCore.Relational/EntityType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Logitar.Identity.EntityFrameworkCore.Relational;

public static class EntityType
{
public const string ApiKey = "ApiKey";
public const string OneTimePassword = "OneTimePassword";
public const string Role = "Role";
public const string Session = "Session";
public const string User = "User";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using Logitar.Identity.Core.ApiKeys.Events;
using Logitar.Identity.EntityFrameworkCore.Relational.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Logitar.Identity.EntityFrameworkCore.Relational.Handlers;

public sealed class ApiKeyEvents : INotificationHandler<ApiKeyAuthenticated>,
INotificationHandler<ApiKeyCreated>,
INotificationHandler<ApiKeyDeleted>,
INotificationHandler<ApiKeyRoleAdded>,
INotificationHandler<ApiKeyRoleRemoved>,
INotificationHandler<ApiKeyUpdated>
{
private readonly IdentityContext _context;
private readonly ICustomAttributeService _customAttributes;

public ApiKeyEvents(IdentityContext context, ICustomAttributeService customAttributes)
{
_context = context;
_customAttributes = customAttributes;
}

public async Task Handle(ApiKeyAuthenticated @event, CancellationToken cancellationToken)
{
ApiKeyEntity apiKey = await _context.ApiKeys
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The API key entity 'StreamId={@event.StreamId}' could not be found.");

apiKey.Authenticate(@event);

await _context.SaveChangesAsync(cancellationToken);
}

public async Task Handle(ApiKeyCreated @event, CancellationToken cancellationToken)
{
ApiKeyEntity? apiKey = await _context.ApiKeys.AsNoTracking()
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken);
if (apiKey == null)
{
apiKey = new(@event);

_context.ApiKeys.Add(apiKey);

await SaveActorAsync(apiKey, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
}

public async Task Handle(ApiKeyDeleted @event, CancellationToken cancellationToken)
{
ApiKeyEntity? apiKey = await _context.ApiKeys
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken);
if (apiKey != null)
{
_context.ApiKeys.Remove(apiKey);

await DeleteActorAsync(apiKey, cancellationToken);
await _customAttributes.RemoveAsync(EntityType.ApiKey, apiKey.ApiKeyId, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
}

public async Task Handle(ApiKeyRoleAdded @event, CancellationToken cancellationToken)
{
ApiKeyEntity apiKey = await _context.ApiKeys
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The API key entity 'StreamId={@event.StreamId}' could not be found.");

RoleEntity role = await _context.Roles
.SingleOrDefaultAsync(x => x.StreamId == @event.RoleId.Value, cancellationToken)
?? throw new InvalidOperationException($"The role entity 'StreamId={@event.RoleId}' could not be found.");

apiKey.AddRole(role, @event);

await _context.SaveChangesAsync(cancellationToken);
}

public async Task Handle(ApiKeyRoleRemoved @event, CancellationToken cancellationToken)
{
ApiKeyEntity apiKey = await _context.ApiKeys
.Include(x => x.Roles)
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The API key entity 'StreamId={@event.StreamId}' could not be found.");

apiKey.RemoveRole(@event);

await _context.SaveChangesAsync(cancellationToken);
}

public async Task Handle(ApiKeyUpdated @event, CancellationToken cancellationToken)
{
ApiKeyEntity apiKey = await _context.ApiKeys
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The API key entity 'StreamId={@event.StreamId}' could not be found.");

apiKey.Update(@event);

await SaveActorAsync(apiKey, cancellationToken);
await _customAttributes.UpdateAsync(EntityType.ApiKey, apiKey.ApiKeyId, @event.CustomAttributes, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}

private async Task DeleteActorAsync(ApiKeyEntity apiKey, CancellationToken cancellationToken)
{
await SaveActorAsync(apiKey, isDeleted: true, cancellationToken);
}
private async Task SaveActorAsync(ApiKeyEntity apiKey, CancellationToken cancellationToken)
{
await SaveActorAsync(apiKey, isDeleted: false, cancellationToken);
}
private async Task SaveActorAsync(ApiKeyEntity apiKey, bool isDeleted, CancellationToken cancellationToken)
{
ActorEntity? actor = await _context.Actors.SingleOrDefaultAsync(x => x.Id == apiKey.StreamId, cancellationToken);
if (actor == null)
{
actor = new()
{
Id = apiKey.StreamId,
Type = ActorType.ApiKey
};
_context.Actors.Add(actor);
}

actor.IsDeleted = isDeleted;

actor.DisplayName = apiKey.DisplayName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Logitar.Identity.Core.Passwords.Events;
using Logitar.Identity.EntityFrameworkCore.Relational.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Logitar.Identity.EntityFrameworkCore.Relational.Handlers;

public sealed class OneTimePasswordEvents : INotificationHandler<OneTimePasswordCreated>,
INotificationHandler<OneTimePasswordDeleted>,
INotificationHandler<OneTimePasswordUpdated>,
INotificationHandler<OneTimePasswordValidationFailed>,
INotificationHandler<OneTimePasswordValidationSucceeded>
{
private readonly IdentityContext _context;
private readonly ICustomAttributeService _customAttributes;

public OneTimePasswordEvents(IdentityContext context, ICustomAttributeService customAttributes)
{
_context = context;
_customAttributes = customAttributes;
}

public async Task Handle(OneTimePasswordCreated @event, CancellationToken cancellationToken)
{
OneTimePasswordEntity? oneTimePassword = await _context.OneTimePasswords.AsNoTracking()
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken);
if (oneTimePassword == null)
{
oneTimePassword = new(@event);

_context.OneTimePasswords.Add(oneTimePassword);

await _context.SaveChangesAsync(cancellationToken);
}
}

public async Task Handle(OneTimePasswordDeleted @event, CancellationToken cancellationToken)
{
OneTimePasswordEntity? oneTimePassword = await _context.OneTimePasswords
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken);
if (oneTimePassword != null)
{
_context.OneTimePasswords.Remove(oneTimePassword);

await _customAttributes.RemoveAsync(EntityType.OneTimePassword, oneTimePassword.OneTimePasswordId, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
}

public async Task Handle(OneTimePasswordUpdated @event, CancellationToken cancellationToken)
{
OneTimePasswordEntity oneTimePassword = await _context.OneTimePasswords
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The One-Time Password (OTP) entity 'StreamId={@event.StreamId}' could not be found.");

oneTimePassword.Update(@event);

await _customAttributes.UpdateAsync(EntityType.OneTimePassword, oneTimePassword.OneTimePasswordId, @event.CustomAttributes, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}

public async Task Handle(OneTimePasswordValidationFailed @event, CancellationToken cancellationToken)
{
OneTimePasswordEntity oneTimePassword = await _context.OneTimePasswords
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The One-Time Password (OTP) entity 'StreamId={@event.StreamId}' could not be found.");

oneTimePassword.Fail(@event);

await _context.SaveChangesAsync(cancellationToken);
}

public async Task Handle(OneTimePasswordValidationSucceeded @event, CancellationToken cancellationToken)
{
OneTimePasswordEntity oneTimePassword = await _context.OneTimePasswords
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The One-Time Password (OTP) entity 'StreamId={@event.StreamId}' could not be found.");

oneTimePassword.Succeed(@event);

await _context.SaveChangesAsync(cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Logitar.Identity.Core.Roles.Events;
using Logitar.Identity.EntityFrameworkCore.Relational.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Logitar.Identity.EntityFrameworkCore.Relational.Handlers;

public sealed class RoleEvents : INotificationHandler<RoleCreated>,
INotificationHandler<RoleDeleted>,
INotificationHandler<RoleUniqueNameChanged>,
INotificationHandler<RoleUpdated>
{
private readonly IdentityContext _context;
private readonly ICustomAttributeService _customAttributes;

public RoleEvents(IdentityContext context, ICustomAttributeService customAttributes)
{
_context = context;
_customAttributes = customAttributes;
}

public async Task Handle(RoleCreated @event, CancellationToken cancellationToken)
{
RoleEntity? role = await _context.Roles.AsNoTracking()
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken);
if (role == null)
{
role = new(@event);

_context.Roles.Add(role);

await _context.SaveChangesAsync(cancellationToken);
}
}

public async Task Handle(RoleDeleted @event, CancellationToken cancellationToken)
{
RoleEntity? role = await _context.Roles
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken);
if (role != null)
{
_context.Roles.Remove(role);

await _customAttributes.RemoveAsync(EntityType.Role, role.RoleId, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
}

public async Task Handle(RoleUniqueNameChanged @event, CancellationToken cancellationToken)
{
RoleEntity role = await _context.Roles
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The role entity 'StreamId={@event.StreamId}' could not be found.");

role.SetUniqueName(@event);

await _context.SaveChangesAsync(cancellationToken);
}

public async Task Handle(RoleUpdated @event, CancellationToken cancellationToken)
{
RoleEntity role = await _context.Roles
.SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken)
?? throw new InvalidOperationException($"The role entity 'StreamId={@event.StreamId}' could not be found.");

role.Update(@event);

await _customAttributes.UpdateAsync(EntityType.Role, role.RoleId, @event.CustomAttributes, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
}
Loading

0 comments on commit 85ddfdd

Please sign in to comment.