Skip to content

Commit

Permalink
Implementations of the repositories aligning to a specific owner and …
Browse files Browse the repository at this point in the history
…using the currently logged user to filter data
  • Loading branch information
tsutomi committed Oct 16, 2024
1 parent dc17265 commit 97c45ca
Show file tree
Hide file tree
Showing 19 changed files with 847 additions and 6 deletions.
24 changes: 24 additions & 0 deletions src/Deveel.Repository.Core/Data/IHaveOwner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Deveel.Data
{
/// <summary>
/// The contract used to define an object that
/// has an owner.
/// </summary>
public interface IHaveOwner<TKey>
{
/// <summary>
/// Gets the identifier of the owner of
/// the object.
/// </summary>
TKey Owner { get; }

/// <summary>
/// Sets the owner of the object, eventually
/// overriding the current owner.
/// </summary>
/// <param name="owner">
/// The identifier of the new owner.
/// </param>
void SetOwner(TKey owner);
}
}
26 changes: 26 additions & 0 deletions src/Deveel.Repository.Core/Data/IUserAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Deveel.Data
{
/// <summary>
/// A service that provides information about the current user
/// of the application.
/// </summary>
/// <remarks>
/// This contact can be used to retrieve identifier about the
/// user that is currently using the application, such as the
/// username or the user ID.
/// </remarks>
/// <typeparam name="TKey">
/// The type of the key that identifies the user.
/// </typeparam>
public interface IUserAccessor<TKey>
{
/// <summary>
/// Gets the identifier of the current user.
/// </summary>
/// <returns>
/// Returns a string that represents the identifier of the
/// user that is currently using the application.
/// </returns>
TKey? GetUserId();
}
}
24 changes: 24 additions & 0 deletions src/Deveel.Repository.Core/Data/IUserRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Deveel.Data
{
/// <summary>
/// Defines a repository that is bound to a user context,
/// where the entities are owned by a specific user.
/// </summary>
/// <typeparam name="TEntity">
/// The type of the entity handled by the repository.
/// </typeparam>
/// <typeparam name="TKey">
/// The type of the unique identifier of the entity.
/// </typeparam>
/// <typeparam name="TOwnerKey">
/// The type of the key that identifies the owner of the entity.
/// </typeparam>
public interface IUserRepository<TEntity, TKey, TOwnerKey> : IRepository<TEntity, TKey>
where TEntity : class, IHaveOwner<TOwnerKey>
{
/// <summary>
/// Gets the accessor to the user context of the repository.
/// </summary>
IUserAccessor<TOwnerKey> UserAccessor { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Deveel.Data
{
[System.AttributeUsage(System.AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class DataOwnerAttribute : Attribute
{
}
}
29 changes: 24 additions & 5 deletions src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Globalization;

using Finbuckle.MultiTenant;
using Finbuckle.MultiTenant.EntityFrameworkCore;

Expand All @@ -23,7 +21,10 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Deveel.Data {
using System.Globalization;

namespace Deveel.Data
{
/// <summary>
/// A repository that uses an <see cref="DbContext"/> to access the data
/// of the entities.
Expand Down Expand Up @@ -208,6 +209,21 @@ protected void ThrowIfDisposed() {
return (TKey?) getter.GetClrValue(entity);
}

/// <summary>
/// A method that is invoked when an entity is
/// being added to the repository.
/// </summary>
/// <param name="entity">
/// The entity that is being added to the repository.
/// </param>
/// <returns>
/// Returns the entity that will be added to the repository.
/// </returns>
protected virtual TEntity OnAddingEntity(TEntity entity)
{
return entity;
}

/// <inheritdoc/>
public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) {
ThrowIfDisposed();
Expand All @@ -217,7 +233,8 @@ public virtual async Task AddAsync(TEntity entity, CancellationToken cancellatio
Logger.TraceCreatingEntity(typeof(TEntity), TenantId);

try {
Entities.Add(entity);
Entities.Add(OnAddingEntity(entity));

var count = await Context.SaveChangesAsync(cancellationToken);

if (count > 1) {
Expand All @@ -238,7 +255,9 @@ public virtual async Task AddRangeAsync(IEnumerable<TEntity> entities, Cancellat
ThrowIfDisposed();

try {
await Entities.AddRangeAsync(entities, cancellationToken);
var toAdd = entities.Select(OnAddingEntity).ToList();

await Entities.AddRangeAsync(toAdd, cancellationToken);

var count = await Context.SaveChangesAsync(true, cancellationToken);
} catch (Exception ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;

using System.Linq.Expressions;

namespace Deveel.Data
{
/// <summary>
/// Extensions for the <see cref="EntityTypeBuilder{TEntity}"/> class
/// to provide additional configuration for entities that have an owner.
/// </summary>
public static class EntityTypeBuilderExtensions
{
/// <summary>
/// Configures the entity to have a query filter that restricts the
/// data to the owner of the entity.
/// </summary>
/// <typeparam name="TEntity">
/// The type of the entity that has an owner.
/// </typeparam>
/// <typeparam name="TUserKey">
/// The type of the key that identifies the user.
/// </typeparam>
/// <param name="builder">
/// A builder to configure the entity.
/// </param>
/// <param name="userAccessor">
/// A service that provides information about the current user
/// that is using the application.
/// </param>
/// <returns>
/// Returns the builder to continue the configuration.
/// </returns>
/// <seealso cref="HasOwnerFilter{TEntity, TUserKey}(EntityTypeBuilder{TEntity}, string, IUserAccessor{TUserKey})"/>
public static EntityTypeBuilder<TEntity> HasOwnerFilter<TEntity, TUserKey>(this EntityTypeBuilder<TEntity> builder, IUserAccessor<TUserKey> userAccessor)
where TEntity : class, IHaveOwner<TUserKey>
=> builder.HasOwnerFilter("", userAccessor);

/// <summary>
/// Configures the entity to have a query filter that restricts the
/// data to the owner of the entity.
/// </summary>
/// <typeparam name="TEntity">
/// The type of the entity that has an owner.
/// </typeparam>
/// <typeparam name="TUserKey">
/// The type of the key that identifies the user.
/// </typeparam>
/// <param name="builder">
/// A builder to configure the entity.
/// </param>
/// <param name="propertyName">
/// The name of the property that holds the owner identifier.
/// </param>
/// <param name="userAccessor">
/// A service that provides information about the current user
/// that is using the application.
/// </param>
/// <returns>
/// Returns the builder to continue the configuration.
/// </returns>
/// <exception cref="RepositoryException">
/// Throws when the property name is not found in the entity type.
/// </exception>
public static EntityTypeBuilder<TEntity> HasOwnerFilter<TEntity, TUserKey>(this EntityTypeBuilder<TEntity> builder, string propertyName, IUserAccessor<TUserKey> userAccessor)
where TEntity : class, IHaveOwner<TUserKey>
{
if (string.IsNullOrWhiteSpace(propertyName))
{
foreach(var property in builder.Metadata.GetDeclaredProperties())
{
if (Attribute.IsDefined(property.PropertyInfo, typeof(DataOwnerAttribute)))
{
propertyName = property.Name;
break;
}
}
} else {
var fieldMetadata = builder.Metadata.FindDeclaredProperty(propertyName);
if (fieldMetadata == null)
throw new RepositoryException($"The property '{propertyName}' was not found in the entity type '{typeof(TEntity).Name}'");
}

if (string.IsNullOrWhiteSpace(propertyName))
throw new RepositoryException($"The property name was not specified and no property was found in the entity type '{typeof(TEntity).Name}'");

var varRef = Expression.Variable(typeof(TEntity), "x");
var propertyRef = Expression.Property(varRef, propertyName);
var getUserId = Expression.Call(
Expression.Constant(userAccessor),
typeof(IUserAccessor<TUserKey>)
.GetMethod(nameof(IUserAccessor<TUserKey>.GetUserId)));

LambdaExpression lambda;

if (typeof(TUserKey) == typeof(string))
{
lambda = Expression.Lambda(Expression.Equal(propertyRef, getUserId), varRef);

} else
{
var equalsMethod = typeof(TUserKey).GetMethod(nameof(object.Equals), new[] { typeof(object) });
lambda = Expression.Lambda(Expression.Call(propertyRef, equalsMethod, getUserId), varRef);
}

return builder.HasQueryFilter(lambda);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Finbuckle.MultiTenant;

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Deveel.Data
{
/// <summary>
/// An implementation of a repository that is bound to a specific user
/// context, where the entities are owned by a specific user.
/// </summary>
/// <typeparam name="TEntity">
/// The type of the entity managed by the repository.
/// </typeparam>
/// <seealso cref="EntityUserRepository{TEntity, TKey, TUserKey}"/>
public class EntityUserRepository<TEntity> : EntityUserRepository<TEntity, object>
where TEntity : class, IHaveOwner<object>
{
/// <summary>
/// Constructs the repository using the given <see cref="DbContext"/>.
/// </summary>
/// <param name="context">
/// The <see cref="DbContext"/> used to access the data of the entities.
/// </param>
/// <param name="userAccessor">
/// A service used to get the current user context.
/// </param>
/// <param name="logger">
/// A logger used to log the operations of the repository.
/// </param>
public EntityUserRepository(DbContext context, IUserAccessor<object> userAccessor, ILogger<EntityUserRepository<TEntity, object>>? logger = null) : base(context, userAccessor, logger)
{
}

/// <summary>
/// Constructs the repository using the given <see cref="DbContext"/> for
/// a specific tenant.
/// </summary>
/// <param name="context">
/// The <see cref="DbContext"/> used to access the data of the entities.
/// </param>
/// <param name="tenantInfo">
/// The information about the tenant that the repository will use to access the data.
/// </param>
/// <param name="userAccessor">
/// A service used to get the current user context.
/// </param>
/// <param name="logger">
/// A logger used to log the operations of the repository.
/// </param>
public EntityUserRepository(DbContext context, ITenantInfo? tenantInfo, IUserAccessor<object> userAccessor, ILogger<EntityUserRepository<TEntity, object>>? logger = null) : base(context, tenantInfo, userAccessor, logger)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

using Finbuckle.MultiTenant;

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Deveel.Data
{
/// <summary>
/// An implementation of a repository that is bound to a specific user
/// that owns the entities.
/// </summary>
/// <typeparam name="TEntity">
/// The type of the entity managed by the repository.
/// </typeparam>
/// <typeparam name="TUserKey">
/// The type of the key that identifies the owner of the entity.
/// </typeparam>
/// <remarks>
/// This version of the repository is assuming the type of the key of the entity
/// of the repository is <see cref="object"/>.
/// </remarks>
/// <seealso cref="EntityUserRepository{TEntity, TKey, TUserKey}"/>
public class EntityUserRepository<TEntity, TUserKey> : EntityUserRepository<TEntity, object, TUserKey>
where TEntity : class, IHaveOwner<TUserKey>
{
/// <summary>
/// Constructs the repository using the given <see cref="DbContext"/>.
/// </summary>
/// <param name="context">
/// The <see cref="DbContext"/> used to access the data of the entities.
/// </param>
/// <param name="userAccessor">
/// A service used to get the current user context.
/// </param>
/// <param name="logger">
/// A logger used to log the operations of the repository.
/// </param>
public EntityUserRepository(DbContext context, IUserAccessor<TUserKey> userAccessor, ILogger<EntityUserRepository<TEntity, object, TUserKey>>? logger = null) : base(context, userAccessor, logger)
{
}

/// <summary>
/// Constructs the repository using the given <see cref="DbContext"/> for
/// the given tenant.
/// </summary>
/// <param name="context">
/// The <see cref="DbContext"/> used to access the data of the entities.
/// </param>
/// <param name="tenantInfo">
/// The information about the tenant that the repository will use to access the data.
/// </param>
/// <param name="userAccessor">
/// A service used to get the current user context.
/// </param>
/// <param name="logger">
/// A logger used to log the operations of the repository.
/// </param>
public EntityUserRepository(DbContext context, ITenantInfo? tenantInfo, IUserAccessor<TUserKey> userAccessor, ILogger<EntityUserRepository<TEntity, object, TUserKey>>? logger = null) : base(context, tenantInfo, userAccessor, logger)
{
}
}
}
Loading

0 comments on commit 97c45ca

Please sign in to comment.