Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Entities Repositories #42

Merged
merged 5 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
run: sudo apt-get install -y libsqlite3-mod-spatialite

- name: Restore dependencies
run: dotnet restore
run: dotnet restore -p:TargetFrameworks=${{ env.DOTNET_FX_VERSION }}

- name: Build
run: dotnet build --no-restore -c Release -f ${{ env.DOTNET_FX_VERSION }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
run: sudo apt-get install -y libsqlite3-mod-spatialite

- name: Restore dependencies
run: dotnet restore
run: dotnet restore -p:TargetFrameworks=${{ env.DOTNET_FX_VERSION }}

- name: Build
run: dotnet build --no-restore --version-suffix $GITHUB_RUN_ID -c Release -f ${{ env.DOTNET_FX_VERSION }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ jobs:
# run: dotnet nuget add source "https://nuget.pkg.github.com/deveel/index.json" -n "Deveel GitHub" -u ${{ secrets.DEVEEL_NUGET_USER }} -p ${{ secrets.DEVEEL_NUGET_TOKEN }} --store-password-in-clear-text

- name: Restore dependencies
run: dotnet restore
run: dotnet restore -p:TargetFrameworks=${{ env.DOTNET_FX_VERSION }}

- name: Build
run: dotnet build --no-restore -c Release
run: dotnet build --no-restore -c Release -f ${{ env.DOTNET_FX_VERSION }}

- name: Test
run: dotnet test --no-build --verbosity normal -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
run: dotnet test --no-build --verbosity normal -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover -f ${{ env.DOTNET_FX_VERSION }}

- name: Collect to Codecov
uses: codecov/codecov-action@v3
Expand Down
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

Check warning on line 4 in src/Deveel.Repository.EntityFramework/Data/DataOwnerAttribute.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x)

Missing XML comment for publicly visible type or member 'DataOwnerAttribute'

Check warning on line 4 in src/Deveel.Repository.EntityFramework/Data/DataOwnerAttribute.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x)

Missing XML comment for publicly visible type or member 'DataOwnerAttribute'
{
}
}
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 @@
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 @@
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 @@
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 Expand Up @@ -524,7 +543,7 @@
/// </param>
protected virtual void Dispose(bool disposing) {
if (!disposedValue) {
Context = null;

Check warning on line 546 in src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x)

Cannot convert null literal to non-nullable reference type.

Check warning on line 546 in src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x)

Cannot convert null literal to non-nullable reference type.

Check warning on line 546 in src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x)

Cannot convert null literal to non-nullable reference type.
disposedValue = true;
}
}
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)))

Check warning on line 71 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x)

Possible null reference argument for parameter 'element' in 'bool Attribute.IsDefined(MemberInfo element, Type attributeType)'.

Check warning on line 71 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x)

Possible null reference argument for parameter 'element' in 'bool Attribute.IsDefined(MemberInfo element, Type attributeType)'.

Check warning on line 71 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x)

Possible null reference argument for parameter 'element' in 'bool Attribute.IsDefined(MemberInfo element, Type attributeType)'.
{
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>)

Check warning on line 90 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x)

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method)'.

Check warning on line 90 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x)

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method)'.

Check warning on line 90 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x)

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method)'.
.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);

Check warning on line 102 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x)

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method, params Expression[]? arguments)'.

Check warning on line 102 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x)

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method, params Expression[]? arguments)'.

Check warning on line 102 in src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x)

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method, params Expression[]? arguments)'.
}

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)
{
}
}
}
Loading
Loading