diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bd31997..c803ab3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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 }}
diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
index e624ca7..40488a5 100644
--- a/.github/workflows/pr-build.yml
+++ b/.github/workflows/pr-build.yml
@@ -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 }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f45c399..e164414 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/src/Deveel.Repository.Core/Data/IHaveOwner.cs b/src/Deveel.Repository.Core/Data/IHaveOwner.cs
new file mode 100644
index 0000000..1749341
--- /dev/null
+++ b/src/Deveel.Repository.Core/Data/IHaveOwner.cs
@@ -0,0 +1,24 @@
+namespace Deveel.Data
+{
+ ///
+ /// The contract used to define an object that
+ /// has an owner.
+ ///
+ public interface IHaveOwner
+ {
+ ///
+ /// Gets the identifier of the owner of
+ /// the object.
+ ///
+ TKey Owner { get; }
+
+ ///
+ /// Sets the owner of the object, eventually
+ /// overriding the current owner.
+ ///
+ ///
+ /// The identifier of the new owner.
+ ///
+ void SetOwner(TKey owner);
+ }
+}
diff --git a/src/Deveel.Repository.Core/Data/IUserAccessor.cs b/src/Deveel.Repository.Core/Data/IUserAccessor.cs
new file mode 100644
index 0000000..ef4c0cb
--- /dev/null
+++ b/src/Deveel.Repository.Core/Data/IUserAccessor.cs
@@ -0,0 +1,26 @@
+namespace Deveel.Data
+{
+ ///
+ /// A service that provides information about the current user
+ /// of the application.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// The type of the key that identifies the user.
+ ///
+ public interface IUserAccessor
+ {
+ ///
+ /// Gets the identifier of the current user.
+ ///
+ ///
+ /// Returns a string that represents the identifier of the
+ /// user that is currently using the application.
+ ///
+ TKey? GetUserId();
+ }
+}
diff --git a/src/Deveel.Repository.Core/Data/IUserRepository.cs b/src/Deveel.Repository.Core/Data/IUserRepository.cs
new file mode 100644
index 0000000..a3c554f
--- /dev/null
+++ b/src/Deveel.Repository.Core/Data/IUserRepository.cs
@@ -0,0 +1,24 @@
+namespace Deveel.Data
+{
+ ///
+ /// Defines a repository that is bound to a user context,
+ /// where the entities are owned by a specific user.
+ ///
+ ///
+ /// The type of the entity handled by the repository.
+ ///
+ ///
+ /// The type of the unique identifier of the entity.
+ ///
+ ///
+ /// The type of the key that identifies the owner of the entity.
+ ///
+ public interface IUserRepository : IRepository
+ where TEntity : class, IHaveOwner
+ {
+ ///
+ /// Gets the accessor to the user context of the repository.
+ ///
+ IUserAccessor UserAccessor { get; }
+ }
+}
diff --git a/src/Deveel.Repository.EntityFramework/Data/DataOwnerAttribute.cs b/src/Deveel.Repository.EntityFramework/Data/DataOwnerAttribute.cs
new file mode 100644
index 0000000..5fa775a
--- /dev/null
+++ b/src/Deveel.Repository.EntityFramework/Data/DataOwnerAttribute.cs
@@ -0,0 +1,7 @@
+namespace Deveel.Data
+{
+ [System.AttributeUsage(System.AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
+ public sealed class DataOwnerAttribute : Attribute
+ {
+ }
+}
diff --git a/src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs b/src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs
index 830885d..d894b96 100644
--- a/src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs
+++ b/src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs
@@ -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;
@@ -23,7 +21,10 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-namespace Deveel.Data {
+using System.Globalization;
+
+namespace Deveel.Data
+{
///
/// A repository that uses an to access the data
/// of the entities.
@@ -208,6 +209,21 @@ protected void ThrowIfDisposed() {
return (TKey?) getter.GetClrValue(entity);
}
+ ///
+ /// A method that is invoked when an entity is
+ /// being added to the repository.
+ ///
+ ///
+ /// The entity that is being added to the repository.
+ ///
+ ///
+ /// Returns the entity that will be added to the repository.
+ ///
+ protected virtual TEntity OnAddingEntity(TEntity entity)
+ {
+ return entity;
+ }
+
///
public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) {
ThrowIfDisposed();
@@ -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) {
@@ -238,7 +255,9 @@ public virtual async Task AddRangeAsync(IEnumerable 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) {
diff --git a/src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs b/src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs
new file mode 100644
index 0000000..a8aaa05
--- /dev/null
+++ b/src/Deveel.Repository.EntityFramework/Data/EntityTypeBuilderExtensions.cs
@@ -0,0 +1,108 @@
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+using System.Linq.Expressions;
+
+namespace Deveel.Data
+{
+ ///
+ /// Extensions for the class
+ /// to provide additional configuration for entities that have an owner.
+ ///
+ public static class EntityTypeBuilderExtensions
+ {
+ ///
+ /// Configures the entity to have a query filter that restricts the
+ /// data to the owner of the entity.
+ ///
+ ///
+ /// The type of the entity that has an owner.
+ ///
+ ///
+ /// The type of the key that identifies the user.
+ ///
+ ///
+ /// A builder to configure the entity.
+ ///
+ ///
+ /// A service that provides information about the current user
+ /// that is using the application.
+ ///
+ ///
+ /// Returns the builder to continue the configuration.
+ ///
+ ///
+ public static EntityTypeBuilder HasOwnerFilter(this EntityTypeBuilder builder, IUserAccessor userAccessor)
+ where TEntity : class, IHaveOwner
+ => builder.HasOwnerFilter("", userAccessor);
+
+ ///
+ /// Configures the entity to have a query filter that restricts the
+ /// data to the owner of the entity.
+ ///
+ ///
+ /// The type of the entity that has an owner.
+ ///
+ ///
+ /// The type of the key that identifies the user.
+ ///
+ ///
+ /// A builder to configure the entity.
+ ///
+ ///
+ /// The name of the property that holds the owner identifier.
+ ///
+ ///
+ /// A service that provides information about the current user
+ /// that is using the application.
+ ///
+ ///
+ /// Returns the builder to continue the configuration.
+ ///
+ ///
+ /// Throws when the property name is not found in the entity type.
+ ///
+ public static EntityTypeBuilder HasOwnerFilter(this EntityTypeBuilder builder, string propertyName, IUserAccessor userAccessor)
+ where TEntity : class, IHaveOwner
+ {
+ 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)
+ .GetMethod(nameof(IUserAccessor.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);
+ }
+ }
+}
diff --git a/src/Deveel.Repository.EntityFramework/Data/EntityUserRepository_T.cs b/src/Deveel.Repository.EntityFramework/Data/EntityUserRepository_T.cs
new file mode 100644
index 0000000..7aa8e30
--- /dev/null
+++ b/src/Deveel.Repository.EntityFramework/Data/EntityUserRepository_T.cs
@@ -0,0 +1,55 @@
+using Finbuckle.MultiTenant;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Deveel.Data
+{
+ ///
+ /// An implementation of a repository that is bound to a specific user
+ /// context, where the entities are owned by a specific user.
+ ///
+ ///
+ /// The type of the entity managed by the repository.
+ ///
+ ///
+ public class EntityUserRepository : EntityUserRepository
+ where TEntity : class, IHaveOwner