diff --git a/Configurations/XApiKeyConfiguration.cs b/Configurations/XApiKeyConfiguration.cs
index f7dae46..c67b3f7 100644
--- a/Configurations/XApiKeyConfiguration.cs
+++ b/Configurations/XApiKeyConfiguration.cs
@@ -23,7 +23,7 @@ namespace xIds.Configurations
///
/// Enable audit logging
///
- public bool EnableAuditLog { get; set; } = true;
+ public bool EnableAuditLog { get; set; } = false;
///
/// Default rate limit (requests per minute)
diff --git a/Constants/ConfigurationNodeNames.cs b/Constants/ConfigurationNodeNames.cs
index 9a350de..f317b30 100644
--- a/Constants/ConfigurationNodeNames.cs
+++ b/Constants/ConfigurationNodeNames.cs
@@ -4,6 +4,7 @@ namespace xIds.Constants
{
public const string DB_SEED_NODE_NAME = "DbSeeder";
public const string PROVIDER_NODE_NAME = "DbProvider";
+ public const string APIKEY_NODE_NAME = "ApiKeyConfiguration";
public const string IDENTITY_NODE_NAME = "IdentityConfiguration";
public const string DATA_SERVICE_NODE_NAME = "DataServiceConfiguration";
public const string IDENTITY_RESOURCE_NODE_NAME = "IdentityResourceConfiguration";
diff --git a/DI/XDIHelperExtension.cs b/DI/XDIHelperExtension.cs
index b964d19..a39d80c 100644
--- a/DI/XDIHelperExtension.cs
+++ b/DI/XDIHelperExtension.cs
@@ -1,31 +1,31 @@
-using System.Collections.Generic;
-using IdentityServer4;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.HttpOverrides;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using xCommons.Authorization;
-using xCommons.Constants;
-using xCommons.Extensions;
-using xExceptions.Constants;
-using xIdentityHelper;
-using xIdentityModels;
-using xIdentityModels.Configurations;
-using xIds.Configurations;
+using xIds.Helpers;
using xIds.Constants;
using xIds.DbContext;
-using xIds.Extensions;
-using xIds.Helpers;
-using xIds.Interfaces;
using xIds.Providers;
+using IdentityServer4;
+using xIdentityHelper;
+using xIdentityModels;
+using xIds.Extensions;
+using xIds.Interfaces;
using xIds.Validators;
+using xCommons.Constants;
using xMessageService.DI;
-using xMessageService.Interfaces;
using xStorageService.DI;
+using xCommons.Extensions;
+using xIds.Configurations;
+using xExceptions.Constants;
+using xCommons.Authorization;
+using System.Collections.Generic;
+using xMessageService.Interfaces;
using xStorageService.Interfaces;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Logging;
+using Microsoft.AspNetCore.Identity;
+using xIdentityModels.Configurations;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
namespace xIds.DI
{
@@ -80,7 +80,7 @@ namespace xIds.DI
}
///
- /// Retrive XDataService Configurations
+ /// Retrive XDataService Configurations ...
///
///
///
@@ -107,6 +107,26 @@ namespace xIds.DI
return result;
}
+ ///
+ /// Retrieve XApiKeyConfiguration Configurations ...
+ ///
+ ///
+ ///
+ public static XApiKeyConfiguration GetXApiKeyConfiguration(this IConfiguration source)
+ {
+ //
+ var section = source
+ .GetSection(Constants.ConfigurationNodeNames.APIKEY_NODE_NAME);
+ var result = section.Get();
+ if (result.IsNull())
+ {
+ result = new XApiKeyConfiguration();
+ }
+
+ //
+ return result;
+ }
+
///
/// Register XDataService Configuration
///
diff --git a/DbContext/XIdentityDbContext.cs b/DbContext/XIdentityDbContext.cs
index 7d01345..5efe4c7 100644
--- a/DbContext/XIdentityDbContext.cs
+++ b/DbContext/XIdentityDbContext.cs
@@ -15,7 +15,7 @@ namespace xIds.DbContext
public DbSet Devices { get; set; }
public DbSet ApiKeys { get; set; }
public DbSet Avatars { get; set; }
- public DbSet ApiKeyUsage { get; set; }
+ public DbSet ApiKeyUsages { get; set; }
public DbSet Applications { get; set; }
public DbSet BannedDevices { get; set; }
public DbSet Followers { get; set; }
diff --git a/Extensions/IdentityExtensions.cs b/Extensions/IdentityExtensions.cs
index ffc7c42..6f01dec 100644
--- a/Extensions/IdentityExtensions.cs
+++ b/Extensions/IdentityExtensions.cs
@@ -1,6 +1,6 @@
-using Microsoft.Extensions.DependencyInjection;
-using xIds.Interfaces;
using xIds.Providers;
+using xIds.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
namespace xIds.Extensions
{
diff --git a/Helpers/XApiKeyHelper.cs b/Helpers/XApiKeyHelper.cs
new file mode 100644
index 0000000..09d5bb7
--- /dev/null
+++ b/Helpers/XApiKeyHelper.cs
@@ -0,0 +1,68 @@
+namespace xIds.Helpers
+{
+ ///
+ /// Api Key Generating Helper Class ...
+ ///
+ public static class XApiKeyHelper
+ {
+ ///
+ /// Generate an Api Key ...
+ ///
+ ///
+ public static (string plainKey, string hash, string prefix) Generate()
+ {
+ //
+ // Generate 32 bytes of cryptographically secure random data
+ var randomBytes = new byte[32];
+ using (var rng = RandomNumberGenerator.Create())
+ {
+ rng.GetBytes(randomBytes);
+ }
+
+ //
+ // Format: xapp_{base64url}
+ var plainKey = $"xapp_{Convert.ToBase64String(randomBytes)
+ .Replace("+", "-").Replace("/", "_").TrimEnd('=')}";
+
+ //
+ // Hash with SHA256 for storage (never store plain key)
+ using (var sha256 = SHA256.Create())
+ {
+ //
+ var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(plainKey));
+ var hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
+
+ //
+ // Prefix for quick identification (first 12 chars)
+ var prefix = plainKey.Substring(0, 16) + "...";
+
+ //
+ return (plainKey, hash, prefix);
+ }
+ }
+
+ ///
+ /// Verify Generating Api Key ...
+ ///
+ ///
+ ///
+ ///
+ public static bool Verify(
+ string plainKey,
+ string storedHash
+ )
+ {
+ //
+ using (var sha256 = SHA256.Create())
+ {
+ //
+ var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(plainKey));
+ var computedHash = BitConverter.ToString(hashBytes)
+ .Replace("-", "").ToLower();
+
+ //
+ return computedHash == storedHash;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Helpers/XIdentityHelper.cs b/Helpers/XIdentityHelper.cs
index a8b8fd1..e5283b9 100644
--- a/Helpers/XIdentityHelper.cs
+++ b/Helpers/XIdentityHelper.cs
@@ -1,13 +1,13 @@
using System;
-using System.IdentityModel.Tokens.Jwt;
-using Microsoft.IdentityModel.Tokens;
-using xCommons.Extensions;
-using xCommons.Providers;
-using xExceptions.Constants;
-using xIdentityModels.Configurations;
-using xIdentityModels.Extensions;
-using xIdentityModels.Models;
using xIds.Interfaces;
+using xCommons.Providers;
+using xCommons.Extensions;
+using xExceptions.Constants;
+using xIdentityModels.Models;
+using xIdentityModels.Extensions;
+using Microsoft.IdentityModel.Tokens;
+using xIdentityModels.Configurations;
+using System.IdentityModel.Tokens.Jwt;
namespace xIds.Helpers
{
diff --git a/Interfaces/IXApplicationProvider.cs b/Interfaces/IXApplicationProvider.cs
index dfc28c5..f176185 100644
--- a/Interfaces/IXApplicationProvider.cs
+++ b/Interfaces/IXApplicationProvider.cs
@@ -27,7 +27,7 @@ namespace xIds.Interfaces
///
Task AddApplicationAsync(
XApplicationDto item,
- XUserClaimsInfoDto userInfo,
+ XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
);
@@ -117,12 +117,14 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
Task FindOneApplicationAsync(
Expression> predicate,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Func, IIncludableQueryable> includeBuilder = null,
CancellationToken cancellationToken = default
);
@@ -132,6 +134,7 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
@@ -139,6 +142,7 @@ namespace xIds.Interfaces
Task> FindManyApplicationAsync(
Expression> predicate,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Func, IOrderedQueryable> orderBuilder = null,
Func, IIncludableQueryable> includeBuilder = null,
CancellationToken cancellationToken = default
@@ -149,6 +153,7 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
@@ -157,6 +162,7 @@ namespace xIds.Interfaces
Task> QueryApplicationAsync(
XQuery query,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Expression> predicate = null,
Func, IOrderedQueryable> orderBuilder = null,
Func, IIncludableQueryable> includeBuilder = null,
@@ -175,7 +181,7 @@ namespace xIds.Interfaces
///
Task AddApiKeyAsync(
XApiKey item,
- XUserClaimsInfoDto userInfo,
+ XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
);
@@ -265,12 +271,14 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
Task FindOneApiKeyAsync(
Expression> predicate,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Func, IIncludableQueryable> includeBuilder = null,
CancellationToken cancellationToken = default
);
@@ -280,6 +288,7 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
@@ -287,6 +296,7 @@ namespace xIds.Interfaces
Task> FindManyApiKeyAsync(
Expression> predicate,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Func, IOrderedQueryable> orderBuilder = null,
Func, IIncludableQueryable> includeBuilder = null,
CancellationToken cancellationToken = default
@@ -297,6 +307,7 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
@@ -305,6 +316,7 @@ namespace xIds.Interfaces
Task> QueryApiKeyAsync(
XQuery query,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Expression> predicate = null,
Func, IOrderedQueryable> orderBuilder = null,
Func, IIncludableQueryable> includeBuilder = null,
@@ -323,7 +335,7 @@ namespace xIds.Interfaces
///
Task AddApiKeyUsageAsync(
XApiKeyUsageDto item,
- XUserClaimsInfoDto userInfo,
+ XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
);
@@ -413,12 +425,14 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
Task FindOneApiKeyUsageAsync(
Expression> predicate,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Func, IIncludableQueryable> includeBuilder = null,
CancellationToken cancellationToken = default
);
@@ -428,6 +442,7 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
@@ -435,6 +450,7 @@ namespace xIds.Interfaces
Task> FindManyApiKeyUsageAsync(
Expression> predicate,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Func, IOrderedQueryable> orderBuilder = null,
Func, IIncludableQueryable> includeBuilder = null,
CancellationToken cancellationToken = default
@@ -445,6 +461,7 @@ namespace xIds.Interfaces
///
///
///
+ ///
///
///
///
@@ -453,6 +470,7 @@ namespace xIds.Interfaces
Task> QueryApiKeyUsageAsync(
XQuery query,
bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
Expression> predicate = null,
Func, IOrderedQueryable> orderBuilder = null,
Func, IIncludableQueryable> includeBuilder = null,
@@ -462,10 +480,217 @@ namespace xIds.Interfaces
//
#region Custom Actions ...
+ ///
+ /// Generate a New ApiKey and Add It to DB ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task CreateApiKey(
+ int applicationId,
+ TimeSpan? expiration = null,
+ IEnumerable scopes = null,
+ IEnumerable allowedIPs = null,
+ int? rateLimit = null,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Revoke Specified Api Key ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task RevokeApiKey(
+ Guid apiKeyId,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Renew Specified Api Key ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task RotateApiKey(
+ Guid apiKeyId,
+ TimeSpan? newExpiration = null,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Retrieve All Api Keys for Specified Application ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task> GetApplicationApiKeys(
+ Guid applicationId,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Validate Specified Api Key in based on ClientIP ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task ValidateApiKey(
+ string apiKey,
+ string clientIP,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ );
#endregion
//
#region Enrichers ...
+ ///
+ /// Enrich Api Key ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichAsync(
+ XApiKey source,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich Api Key ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichAsync(
+ XApiKeyDto source,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich Api Key Usage ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichAsync(
+ XApiKeyUsage source,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich Application ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichAsync(
+ XApplication source,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich Api Key Usage ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichAsync(
+ XApiKeyUsageDto source,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich Application ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichAsync(
+ XApplicationDto source,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich a Collection of Api Key(s) ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich a Collection of Api Key(s) ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich a Collection of Api Key Usage(s) ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich a Collection of Application(s) ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich a Collection of Api Key Usage(s) ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Enrich a Collection of Application(s) ...
+ ///
+ ///
+ ///
+ ///
+ Task EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ );
#endregion
}
}
\ No newline at end of file
diff --git a/Providers/XApplicationProvider.cs b/Providers/XApplicationProvider.cs
new file mode 100644
index 0000000..4216a72
--- /dev/null
+++ b/Providers/XApplicationProvider.cs
@@ -0,0 +1,280 @@
+using System;
+using System.Linq;
+using xModels.Dtos;
+using xIds.DbContext;
+using xIds.Interfaces;
+using xCommons.Extensions;
+using xIds.Configurations;
+using xIdentityModels.Dtos;
+using xExceptions.Constants;
+using System.Threading.Tasks;
+using xIdentityModels.Models;
+using System.Linq.Expressions;
+using IdentityServer4.Extensions;
+using xIdentityModels.Extensions;
+using Microsoft.EntityFrameworkCore.Query;
+
+namespace xIds.Providers
+{
+ ///
+ /// a Service For Manage Security Applications and also Api Key(s) ...
+ ///
+ public partial class XApplicationProvider : IXApplicationProvider
+ {
+ private readonly string permittedRole;
+ private readonly XIdentityDbContext dbContext;
+ private readonly IXIdentityManager identityManager;
+ private readonly XDataServiceConfiguration configuration;
+ private readonly XApiKeyConfiguration apiKeyConfiguration;
+
+ public XApplicationProvider(
+ XIdentityDbContext dbContext,
+ IXIdentityManager identityManager,
+ XDataServiceConfiguration configuration,
+ XApiKeyConfiguration apiKeyConfiguration,
+ string permittedRole = "admin"
+ )
+ {
+ //
+ this.dbContext = dbContext;
+ this.permittedRole = permittedRole;
+ this.configuration = configuration;
+ this.identityManager = identityManager;
+ this.apiKeyConfiguration = apiKeyConfiguration;
+ }
+
+ ///
+ /// Retrieve Owner ...
+ ///
+ ///
+ ///
+ public async Task GetOwner(string ownerId)
+ {
+ //
+ if (ownerId.IsNullOrEmpty())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ var user = await identityManager.GetUserAsync(ownerId);
+ XPersonDto result = user.ToXPerson();
+ if (result.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ return result;
+ }
+
+ ///
+ /// Check Specifed User Info is Owned for Action or not ...
+ ///
+ ///
+ ///
+ ///
+ public bool IsOwned(
+ XApplication source,
+ XUserClaimsInfoDto userInfo = null
+ )
+ {
+ //
+ var result =
+ !source.IsNullOrDefault() &&
+ !userInfo.IsNullOrDefault() &&
+ (userInfo.UserId == source.OwnerId ||
+ userInfo.Roles
+ .Any(r =>
+ r.ToNormalString() == permittedRole.ToNormalString()));
+
+ //
+ return result;
+ }
+
+ ///
+ /// Check Specifed User Info is Owned for Action or not ...
+ ///
+ ///
+ ///
+ ///
+ public bool IsOwned(
+ XApplicationDto source,
+ XUserClaimsInfoDto userInfo = null
+ )
+ {
+ //
+ var result =
+ !source.IsNullOrDefault() &&
+ !userInfo.IsNullOrDefault() &&
+ (userInfo.UserId == source.OwnerId ||
+ userInfo.Roles
+ .Any(r =>
+ r.ToNormalString() == permittedRole.ToNormalString()));
+
+ //
+ return result;
+ }
+
+ ///
+ /// Normalize an XQuery based on Data Service Configurations ...
+ ///
+ ///
+ ///
+ ///
+ public XQuery NormalizeQuery(
+ XQuery source,
+ XDataServiceConfiguration configuration //
+ )
+ {
+ //
+ if (source.IsNull())
+ {
+ source = new XQuery();
+ }
+
+ //
+ // Page Size Normalization ...
+ if (source.PageSize < configuration.PagingConfiguration.MinAvailablePageSize)
+ {
+ source.PageSize = configuration.PagingConfiguration.MinAvailablePageSize;
+ }
+ if (source.PageSize > configuration.PagingConfiguration.MaxAvailablePageSize)
+ {
+ source.PageSize = configuration.PagingConfiguration.MaxAvailablePageSize;
+ }
+
+ //
+ return source;
+ }
+
+ ///
+ /// Prepare a Queryable of Items ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IQueryable GetApplicationsQeryable(
+ Expression> predicate = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null
+ )
+ {
+ //
+ // Global Result ...
+ var result = dbContext.Applications.AsQueryable();
+
+ //
+ // Handle Filtering ...
+ if (!predicate.IsNull())
+ {
+ result = result.Where(predicate);
+ }
+
+ //
+ // Handle Including ...
+ if (!includeBuilder.IsNull())
+ {
+ result = includeBuilder(result);
+ }
+
+ //
+ // Handle Ordering ...
+ if (!orderBuilder.IsNull())
+ {
+ result = orderBuilder(result);
+ }
+
+ //
+ // Return result ...
+ return result;
+ }
+
+ ///
+ /// Prepare a Queryable of Items ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IQueryable GetApiKeysQeryable(
+ Expression> predicate = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null
+ )
+ {
+ //
+ // Global Result ...
+ var result = dbContext.ApiKeys.AsQueryable();
+
+ //
+ // Handle Filtering ...
+ if (!predicate.IsNull())
+ {
+ result = result.Where(predicate);
+ }
+
+ //
+ // Handle Including ...
+ if (!includeBuilder.IsNull())
+ {
+ result = includeBuilder(result);
+ }
+
+ //
+ // Handle Ordering ...
+ if (!orderBuilder.IsNull())
+ {
+ result = orderBuilder(result);
+ }
+
+ //
+ // Return result ...
+ return result;
+ }
+
+ ///
+ /// Prepare a Queryable of Items ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IQueryable GetApiKeyUsagesQeryable(
+ Expression> predicate = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null
+ )
+ {
+ //
+ // Global Result ...
+ var result = dbContext.ApiKeyUsage.AsQueryable();
+
+ //
+ // Handle Filtering ...
+ if (!predicate.IsNull())
+ {
+ result = result.Where(predicate);
+ }
+
+ //
+ // Handle Including ...
+ if (!includeBuilder.IsNull())
+ {
+ result = includeBuilder(result);
+ }
+
+ //
+ // Handle Ordering ...
+ if (!orderBuilder.IsNull())
+ {
+ result = orderBuilder(result);
+ }
+
+ //
+ // Return result ...
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Providers/XApplicationProvider/XApplicationProvider+ApiKey.cs b/Providers/XApplicationProvider/XApplicationProvider+ApiKey.cs
new file mode 100644
index 0000000..2df3d7b
--- /dev/null
+++ b/Providers/XApplicationProvider/XApplicationProvider+ApiKey.cs
@@ -0,0 +1,773 @@
+using System;
+using System.Linq;
+using xModels.Dtos;
+using System.Threading;
+using xCommons.Extensions;
+using xIdentityModels.Dtos;
+using xExceptions.Constants;
+using System.Threading.Tasks;
+using xIdentityModels.Models;
+using System.Linq.Expressions;
+using xDataService.Extensions;
+using System.Collections.Generic;
+using xIdentityModels.Extensions;
+using Microsoft.EntityFrameworkCore;
+using System.Runtime.CompilerServices;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace xIds.Providers
+{
+ public partial class XApplicationProvider
+ {
+ //
+ #region ApiKey ...
+ ///
+ /// Add Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task AddApiKeyAsync(
+ XApiKey item,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (!item.IsValidForAdd())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Permission ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ // Check Application Exists ...
+ var application = await GetApplicationAsync(
+ userInfo: userInfo,
+ includeBuilder: null,
+ id: item.ApplicationId,
+ ignoreSoftDeleteds: true,
+ cancellationToken: cancellationToken
+ );
+ if (application.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Validate Owner ...
+ if (!IsOwned(application, userInfo))
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ // Check COunt of API Keys ...
+ var appApiKeysCount = await dbContext.ApiKeys.CountAsync(
+ cancellationToken: cancellationToken,
+ predicate: x => x.ApplicationId == item.ApplicationId
+ );
+ if (appApiKeysCount >= apiKeyConfiguration.MaxKeysPerApplication)
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ item.CreatedOn = DateTime.UtcNow;
+
+ //
+ var entity = item.MapConvert();
+ if (entity.Id.IsValidIntId())
+ {
+ entity.Id = -1;
+ }
+
+ //
+ var entry = await dbContext.ApiKeys.AddAsync(
+ entity: entity,
+ cancellationToken: cancellationToken
+ );
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+
+ //
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNullOrDefault()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Update Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task UpdateApiKeyAsync(
+ Guid id,
+ XApiKeyDto item,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (
+ id != item.Id ||
+ !id.IsDefaultGuid() ||
+ !item.IsValidForAdd() ||
+ item.IsNullOrDefault() ||
+ userInfo.IsNullOrDefault()
+ )
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Retrieve Exists Entity ...
+ var exists = await dbContext.ApiKeys
+ .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted);
+ if (exists.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Permission Checking ...
+ var application = await GetApplicationAsync(
+ userInfo: userInfo,
+ includeBuilder: null,
+ id: item.ApplicationId,
+ ignoreSoftDeleteds: true,
+ cancellationToken: cancellationToken
+ );
+ if (application.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+ if (!IsOwned(application, userInfo))
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ // Update Data on Exists Entity ...
+ exists = exists.UpdateData(
+ updateWith: item,
+ propertyWhiteList: [
+ nameof(XApiKey.KeyHash),
+ nameof(XApiKey.KeyPrefix),
+ nameof(XApiKey.ExpiresAt),
+ nameof(XApiKey.CreatedOn),
+ nameof(XApiKey.RevokedAt),
+ nameof(XApiKey.RevokedBy),
+ nameof(XApiKey.LastUsedAt),
+ nameof(XApiKey.AllowedIPs),
+ nameof(XApiKey.ApplicationId),
+ nameof(XApiKey.AllowedScopes),
+ nameof(XApiKey.RateLimitPerMinute),
+ ]
+ );
+
+ //
+ var entry = dbContext.ApiKeys.Attach(exists);
+ entry.State = EntityState.Modified;
+
+ //
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNullOrDefault()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Remove Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task RemoveApiKeyAsync(
+ Guid id,
+ bool softDelete = true,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (
+ id.IsDefaultGuid() ||
+ userInfo.IsNullOrDefault()
+ )
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Retrieve Exists Entity ...
+ var exists = await dbContext.ApiKeys
+ .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted);
+ if (exists.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Permission Checking ...
+ var application = await GetApplicationAsync(
+ userInfo: userInfo,
+ includeBuilder: null,
+ id: exists.ApplicationId,
+ ignoreSoftDeleteds: true,
+ cancellationToken: cancellationToken
+ );
+ if (application.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+ if (!IsOwned(application, userInfo))
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ EntityEntry entry = null;
+ if (softDelete)
+ {
+ //
+ exists.Deleted = true;
+ entry = dbContext.ApiKeys.Update(exists);
+ }
+ else
+ {
+ entry = dbContext.ApiKeys.Remove(exists);
+ }
+
+ //
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNull()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve an Item by Providing it's ID ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task GetApiKeyAsync(
+ Guid id,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (id.IsDefaultGuid() ||
+ userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Prepare Condition ...
+ Expression> predicate = x =>
+ !ignoreSoftDeleteds || !x.Deleted;
+
+ //
+ // Retrieve Queryable ...
+ var queryable = GetApiKeysQeryable(
+ orderBuilder: null,
+ predicate: predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entity = await queryable.FirstOrDefaultAsync(
+ predicate: x => x.Id == id,
+ cancellationToken: cancellationToken
+ );
+ if (entity.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Permission Checking ...
+ var application = await GetApplicationAsync(
+ userInfo: userInfo,
+ includeBuilder: null,
+ id: entity.ApplicationId,
+ ignoreSoftDeleteds: true,
+ cancellationToken: cancellationToken
+ );
+ if (application.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+ if (!IsOwned(application, userInfo))
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve all Items ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetAllApiKeyAsync(
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ var applications = await GetAllApplicationAsync(
+ userInfo: userInfo,
+ orderBuilder: null,
+ includeBuilder: null,
+ cancellationToken: cancellationToken,
+ ignoreSoftDeleteds: ignoreSoftDeleteds
+ );
+ applications = applications.Where(a => IsOwned(a, userInfo));
+ var appIds = applications
+ .Select(a => a.Id)
+ .ToList();
+
+ //
+ Expression> predicate = x =>
+ appIds.Contains(x.ApplicationId) &&
+ !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeysQeryable(
+ predicate: predicate,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var items = await queryable.ToListAsync(cancellationToken);
+ var result = await EnrichRangeAsync(
+ sources: items,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve all Items as Async Enumerable ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async IAsyncEnumerable GetAllApiKeyAsAsyncEnumerable(
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ [EnumeratorCancellation]
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ var applications = await GetAllApplicationAsync(
+ userInfo: userInfo,
+ orderBuilder: null,
+ includeBuilder: null,
+ cancellationToken: cancellationToken,
+ ignoreSoftDeleteds: ignoreSoftDeleteds
+ );
+ applications = applications.Where(a => IsOwned(a, userInfo));
+ var appIds = applications
+ .Select(a => a.Id)
+ .ToList();
+
+ //
+ Expression> predicate = x =>
+ appIds.Contains(x.ApplicationId) &&
+ !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeysQeryable(
+ predicate: predicate,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var enumerator = queryable.AsAsyncEnumerable();
+ await foreach (var item in enumerator)
+ {
+ //
+ // Handle Cancellation Token ...
+ if (cancellationToken.IsCancellationRequested)
+ {
+ yield break;
+ }
+
+ //
+ var dto = await EnrichAsync(
+ source: item,
+ cancellationToken: cancellationToken
+ );
+
+ yield return dto;
+ }
+ }
+
+ ///
+ /// Find an Item By Providing a Conditional Expression ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task FindOneApiKeyAsync(
+ Expression> predicate,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeysQeryable(
+ orderBuilder: null,
+ predicate: _predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entity = (await queryable
+ .ToListAsync(cancellationToken))
+ .FirstOrDefault(x => predicate.Compile()(x));
+
+ //
+ // Permission Checking ...
+ var application = await GetApplicationAsync(
+ userInfo: userInfo,
+ includeBuilder: null,
+ id: entity.ApplicationId,
+ ignoreSoftDeleteds: true,
+ cancellationToken: cancellationToken
+ );
+ if (application.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+ if (!IsOwned(application, userInfo))
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Find a Collection of Items By Providing a Conditional Expression ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> FindManyApiKeyAsync(
+ Expression> predicate,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeysQeryable(
+ orderBuilder: null,
+ predicate: _predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entities = (await queryable
+ .ToListAsync(cancellationToken))
+ .Where(x => predicate.Compile()(x))
+ .ToList();
+
+ //
+ var appIds = entities
+ .Select(k => k.ApplicationId)
+ .ToList();
+
+ //
+ foreach (var appId in appIds)
+ {
+ //
+ // Permission Checking ...
+ var application = await GetApplicationAsync(
+ id: appId,
+ userInfo: userInfo,
+ includeBuilder: null,
+ ignoreSoftDeleteds: true,
+ cancellationToken: cancellationToken
+ );
+ if (
+ application.IsNullOrDefault() ||
+ !IsOwned(application, userInfo))
+ {
+ entities = [.. entities.Where(e => e.ApplicationId != appId)];
+ }
+ }
+
+ //
+ var result = await EnrichRangeAsync(
+ sources: entities,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve Items based on XQuery Pagination Structure ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> QueryApiKeyAsync(
+ XQuery query,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Expression> predicate = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Normalize Query ...
+ query = NormalizeQuery(query, configuration);
+
+ //
+ Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeysQeryable(
+ predicate: _predicate,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var items = (await queryable
+ .ToListAsync(cancellationToken))
+ .Where(x => predicate.Compile()(x));
+
+ //
+ var appIds = items
+ .Select(k => k.ApplicationId)
+ .ToList();
+
+ //
+ foreach (var appId in appIds)
+ {
+ //
+ // Permission Checking ...
+ var application = await GetApplicationAsync(
+ id: appId,
+ userInfo: userInfo,
+ includeBuilder: null,
+ ignoreSoftDeleteds: true,
+ cancellationToken: cancellationToken
+ );
+ if (
+ application.IsNullOrDefault() ||
+ !IsOwned(application, userInfo))
+ {
+ items = [.. items.Where(e => e.ApplicationId != appId)];
+ }
+ }
+
+ //
+ var totalItemsCount = items.Count();
+
+ //
+ // Apply Filter ...
+ if (!query.Filter.IsNullOrEmpty())
+ {
+ //
+ items = items
+ .AsEnumerable()
+ .ApplyFilter(query.Filter);
+ }
+ int filteredItemsCount = items.Count();
+
+ //
+ // Count Pages ...
+ var totalPagesCount = query.CountPages(totalItemsCount);
+ var filteredPagesCount = query.CountPages(filteredItemsCount);
+
+ //
+ // Apply Paging and Sorting ...
+ if (totalItemsCount > 0 &&
+ filteredItemsCount > 0
+ )
+ {
+ //
+ // Apply Sorting ...
+ items = items
+ .ToList()
+ .ApplySorting(
+ query.SortBy,
+ query.IsAscending
+ );
+
+ //
+ // Apply Paging ...
+ items = items
+ .ToList()
+ .ApplyPaging(
+ query.Page,
+ query.PageSize
+ );
+ }
+
+ //
+ var resultItems = await EnrichRangeAsync(
+ sources: items,
+ cancellationToken: cancellationToken
+ );
+
+ //
+ // Prepare Result ...
+ var result = new XQueryResult
+ {
+ Page = query.Page,
+ Items = [.. resultItems],
+ PageSize = query.PageSize,
+ TotalPages = totalPagesCount,
+ TotalItems = totalItemsCount,
+ TotalFilteredPages = filteredPagesCount,
+ TotalFilteredItems = filteredItemsCount
+ };
+
+ //
+ return result;
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Providers/XApplicationProvider/XApplicationProvider+ApiKeyUsage.cs b/Providers/XApplicationProvider/XApplicationProvider+ApiKeyUsage.cs
new file mode 100644
index 0000000..b51cc9a
--- /dev/null
+++ b/Providers/XApplicationProvider/XApplicationProvider+ApiKeyUsage.cs
@@ -0,0 +1,531 @@
+using System;
+using System.Linq;
+using xModels.Dtos;
+using System.Threading;
+using xCommons.Extensions;
+using xIdentityModels.Dtos;
+using xExceptions.Constants;
+using System.Threading.Tasks;
+using xIdentityModels.Models;
+using System.Linq.Expressions;
+using xDataService.Extensions;
+using System.Collections.Generic;
+using xIdentityModels.Extensions;
+using Microsoft.EntityFrameworkCore;
+using System.Runtime.CompilerServices;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace xIds.Providers
+{
+ public partial class XApplicationProvider
+ {
+ //
+ #region ApiKeyUsage ...
+ ///
+ /// Add Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task AddApiKeyUsageAsync(
+ XApiKeyUsageDto item,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (!item.IsValidForAdd())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Check ApiKey Exists ...
+ var apiKey = await dbContext.ApiKeys
+ .FirstOrDefaultAsync(x => x.Id == item.ApiKeyId);
+ if (apiKey.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Check apiKey Activation ...
+ if (!apiKey.IsActive())
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ var entity = item.MapConvert();
+ var entry = await dbContext.ApiKeyUsages.AddAsync(
+ entity: entity,
+ cancellationToken: cancellationToken
+ );
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNullOrDefault()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Update Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task UpdateApiKeyUsageAsync(
+ long id,
+ XApiKeyUsageDto item,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (
+ id <= 0 ||
+ id != item.Id ||
+ !item.IsValidForAdd() ||
+ item.IsNullOrDefault()
+ )
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Check Item Exists ...
+ var exists = dbContext.ApiKeyUsages.FirstOrDefault(x => x.Id == id);
+ if (exists.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Update Data on Exists Entity ...
+ exists = exists.UpdateData(
+ updateWith: item,
+ propertyWhiteList: [
+ nameof(XApiKeyUsage.ApiKeyId),
+ nameof(XApiKeyUsage.Endpoint),
+ nameof(XApiKeyUsage.ClientIP),
+ nameof(XApiKeyUsage.HttpMethod),
+ nameof(XApiKeyUsage.StatusCode),
+ nameof(XApiKeyUsage.RequestedOn),
+ nameof(XApiKeyUsage.ResponseTimeMs),
+ ]
+ );
+
+ //
+ var entry = dbContext.ApiKeyUsages.Attach(exists);
+ entry.State = EntityState.Modified;
+
+ //
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNullOrDefault()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Remove Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task RemoveApiKeyUsageAsync(
+ long id,
+ bool softDelete = true,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (id <= 0)
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Retrieve Exists Entity ...
+ var exists = await dbContext.ApiKeyUsages
+ .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted);
+ if (exists.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ EntityEntry entry = null;
+ if (softDelete)
+ {
+ //
+ exists.Deleted = true;
+ entry = dbContext.ApiKeyUsages.Update(exists);
+ }
+ else
+ {
+ entry = dbContext.ApiKeyUsages.Remove(exists);
+ }
+
+ //
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNull()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve an Item by Providing it's ID ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task GetApiKeyUsageAsync(
+ long id,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (id <= 0)
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Prepare Condition ...
+ Expression> predicate = x =>
+ !ignoreSoftDeleteds || !x.Deleted;
+
+ //
+ // Retrieve Queryable ...
+ var queryable = GetApiKeyUsagesQeryable(
+ orderBuilder: null,
+ predicate: predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entity = await queryable.FirstOrDefaultAsync(
+ predicate: x => x.Id == id,
+ cancellationToken: cancellationToken
+ );
+ if (entity.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve all Items ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetAllApiKeyUsageAsync(
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ Expression> predicate = x =>
+ !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeyUsagesQeryable(
+ predicate: predicate,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var items = await queryable.ToListAsync(cancellationToken);
+ var result = await EnrichRangeAsync(
+ sources: items,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve all Items as Async Enumerable ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async IAsyncEnumerable GetAllApiKeyUsageAsAsyncEnumerable(
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ [EnumeratorCancellation]
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ Expression> predicate = x =>
+ !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeyUsagesQeryable(
+ predicate: predicate,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var enumerator = queryable.AsAsyncEnumerable();
+ await foreach (var item in enumerator)
+ {
+ //
+ // Handle Cancellation Token ...
+ if (cancellationToken.IsCancellationRequested)
+ {
+ yield break;
+ }
+
+ //
+ var dto = await EnrichAsync(
+ source: item,
+ cancellationToken: cancellationToken
+ );
+
+ yield return dto;
+ }
+ }
+
+ ///
+ /// Find an Item By Providing a Conditional Expression ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task FindOneApiKeyUsageAsync(
+ Expression> predicate,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeyUsagesQeryable(
+ orderBuilder: null,
+ predicate: _predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entity = (await queryable
+ .ToListAsync(cancellationToken))
+ .FirstOrDefault(x => predicate.Compile()(x));
+
+ //
+ var result = await EnrichAsync(
+ source: entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Find a Collection of Items By Providing a Conditional Expression ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> FindManyApiKeyUsageAsync(
+ Expression> predicate,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted;
+ var queryable = GetApiKeyUsagesQeryable(
+ orderBuilder: null,
+ predicate: _predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entities = (await queryable
+ .ToListAsync(cancellationToken))
+ .Where(x => predicate.Compile()(x))
+ .ToList();
+
+ //
+ var result = await EnrichRangeAsync(
+ sources: entities,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve Items based on XQuery Pagination Structure ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> QueryApiKeyUsageAsync(
+ XQuery query,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Expression> predicate = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Normalize Query ...
+ query = NormalizeQuery(query, configuration);
+
+ //
+ var items = await GetAllApiKeyUsageAsync(
+ userInfo: userInfo,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder,
+ cancellationToken: cancellationToken,
+ ignoreSoftDeleteds: ignoreSoftDeleteds
+ );
+ var totalItemsCount = items.Count();
+
+ //
+ // Apply Filter ...
+ if (!query.Filter.IsNullOrEmpty())
+ {
+ //
+ items = items
+ .ApplyFilter(query.Filter);
+ }
+ int filteredItemsCount = items.Count();
+
+ //
+ // Count Pages ...
+ var totalPagesCount = query.CountPages(totalItemsCount);
+ var filteredPagesCount = query.CountPages(filteredItemsCount);
+
+ //
+ // Apply Paging and Sorting ...
+ if (totalItemsCount > 0 &&
+ filteredItemsCount > 0
+ )
+ {
+ //
+ // Apply Sorting ...
+ items = items
+ .ToList()
+ .ApplySorting(
+ query.SortBy,
+ query.IsAscending
+ );
+
+ //
+ // Apply Paging ...
+ items = items
+ .ToList()
+ .ApplyPaging(
+ query.Page,
+ query.PageSize
+ );
+ }
+
+ //
+ // Prepare Result ...
+ var result = new XQueryResult
+ {
+ Page = query.Page,
+ Items = [.. items],
+ PageSize = query.PageSize,
+ TotalPages = totalPagesCount,
+ TotalItems = totalItemsCount,
+ TotalFilteredPages = filteredPagesCount,
+ TotalFilteredItems = filteredItemsCount
+ };
+
+ //
+ return result;
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Providers/XApplicationProvider/XApplicationProvider+Application.cs b/Providers/XApplicationProvider/XApplicationProvider+Application.cs
new file mode 100644
index 0000000..a402919
--- /dev/null
+++ b/Providers/XApplicationProvider/XApplicationProvider+Application.cs
@@ -0,0 +1,624 @@
+using System;
+using System.Linq;
+using xModels.Dtos;
+using System.Threading;
+using xCommons.Extensions;
+using xIdentityModels.Dtos;
+using xExceptions.Constants;
+using System.Threading.Tasks;
+using xIdentityModels.Models;
+using System.Linq.Expressions;
+using xDataService.Extensions;
+using System.Collections.Generic;
+using xIdentityModels.Extensions;
+using xIdentityHelper.Extensions;
+using Microsoft.EntityFrameworkCore;
+using System.Runtime.CompilerServices;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace xIds.Providers
+{
+ public partial class XApplicationProvider
+ {
+ //
+ #region Application ...
+ ///
+ /// Add Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task AddApplicationAsync(
+ XApplicationDto item,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (!item.IsValidForAdd())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Permission ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ item.OwnerId = userInfo.UserId;
+ item.CreatedOn = DateTime.UtcNow;
+
+ //
+ var entity = item.MapConvert();
+ if (entity.Id.IsValidIntId())
+ {
+ entity.Id = -1;
+ }
+
+ //
+ var entry = await dbContext.Applications.AddAsync(
+ entity: entity,
+ cancellationToken: cancellationToken
+ );
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+
+ //
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNullOrDefault()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Update Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task UpdateApplicationAsync(
+ int id,
+ XApplicationDto item,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (
+ id != item.Id ||
+ !id.IsValidIntId() ||
+ item.IsNullOrDefault() ||
+ userInfo.IsNullOrDefault()
+ )
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Retrieve Exists Entity ...
+ var exists = await dbContext.Applications
+ .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted);
+ if (exists.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Permission Checking ...
+ if (!IsOwned(exists, userInfo))
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ // Update Data on Exists Entity ...
+ exists = exists.UpdateData(
+ updateWith: item,
+ propertyWhiteList: [
+ nameof(XApplication.Name),
+ nameof(XApplication.Type),
+ nameof(XApplication.OwnerId),
+ nameof(XApplication.IsActive),
+ nameof(XApplication.CreatedOn),
+ nameof(XApplication.UpdatedAt),
+ nameof(XApplication.Description),
+ ]
+ );
+
+ //
+ var entry = dbContext.Applications.Attach(exists);
+ entry.State = EntityState.Modified;
+
+ //
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNullOrDefault()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Remove Specified Item ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task RemoveApplicationAsync(
+ int id,
+ bool softDelete = true,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (
+ !id.IsValidIntId() ||
+ userInfo.IsNullOrDefault()
+ )
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Retrieve Exists Entity ...
+ var exists = await dbContext.Applications
+ .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted);
+ if (exists.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Permission Checking ...
+ if (!IsOwned(exists, userInfo))
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ EntityEntry entry = null;
+ if (softDelete)
+ {
+ //
+ exists.Deleted = true;
+ entry = dbContext.Applications.Update(exists);
+ }
+ else
+ {
+ entry = dbContext.Applications.Remove(exists);
+ }
+
+ //
+ var changes = await dbContext.SaveChangesAsync(cancellationToken);
+ if (
+ changes <= 0 ||
+ entry.IsNull() ||
+ entry.Entity.IsNull()
+ )
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ //
+ var result = await EnrichAsync(
+ source: entry.Entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve an Item by Providing it's ID ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task GetApplicationAsync(
+ int id,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (!id.IsValidIntId() ||
+ userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Prepare Condition ...
+ Expression> predicate = x =>
+ x.OwnerId != userInfo.UserId ||
+ !ignoreSoftDeleteds || !x.Deleted;
+ if (userInfo.IsAdmin())
+ {
+ //
+ predicate = x =>
+ !ignoreSoftDeleteds || !x.Deleted;
+ }
+
+ //
+ // Retrieve Queryable ...
+ var queryable = GetApplicationsQeryable(
+ orderBuilder: null,
+ predicate: predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entity = await queryable.FirstOrDefaultAsync(
+ predicate: x => x.Id == id,
+ cancellationToken: cancellationToken
+ );
+
+ //
+ var result = await EnrichAsync(
+ source: entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve all Items ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetAllApplicationAsync(
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Prepare Condition ...
+ Expression> predicate = x =>
+ x.OwnerId == userInfo.UserId &&
+ (!ignoreSoftDeleteds || !x.Deleted);
+ if (userInfo.IsAdmin())
+ {
+ //
+ predicate = x =>
+ !ignoreSoftDeleteds || !x.Deleted;
+ }
+
+ //
+ // Retrieve Queryable ...
+ var queryable = GetApplicationsQeryable(
+ predicate: predicate,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var items = await queryable.ToListAsync(cancellationToken);
+ var result = await EnrichRangeAsync(
+ sources: items,
+ cancellationToken: cancellationToken
+ );
+
+ //
+ return result;
+ }
+
+ ///
+ /// Retrieve all Items as Async Enumerable ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async IAsyncEnumerable GetAllApplicationAsAsyncEnumerable(
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ [EnumeratorCancellation]
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Prepare Condition ...
+ Expression> predicate = x =>
+ x.OwnerId == userInfo.UserId &&
+ (!ignoreSoftDeleteds || !x.Deleted);
+ if (userInfo.IsAdmin())
+ {
+ //
+ predicate = x =>
+ !ignoreSoftDeleteds || !x.Deleted;
+ }
+
+ //
+ // Retrieve Queryable ...
+ var queryable = GetApplicationsQeryable(
+ predicate: predicate,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var enumerator = queryable.AsAsyncEnumerable();
+ await foreach (var item in enumerator)
+ {
+ //
+ // Handle Cancellation Token ...
+ if (cancellationToken.IsCancellationRequested)
+ {
+ yield break;
+ }
+
+ //
+ var dto = await EnrichAsync(
+ source: item,
+ cancellationToken: cancellationToken
+ );
+
+ yield return dto;
+ }
+ }
+
+ ///
+ /// Find an Item By Providing a Conditional Expression ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task FindOneApplicationAsync(
+ Expression> predicate,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted;
+ if (!userInfo.IsAdmin())
+ {
+ _predicate = x =>
+ x.OwnerId == userInfo.UserId &&
+ (!ignoreSoftDeleteds || !x.Deleted);
+ }
+
+ //
+ var queryable = GetApplicationsQeryable(
+ orderBuilder: null,
+ predicate: _predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entity = (await queryable
+ .ToListAsync(cancellationToken))
+ .FirstOrDefault(x => predicate.Compile()(x));
+
+ //
+ var result = await EnrichAsync(
+ source: entity,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Find a Collection of Items By Providing a Conditional Expression ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> FindManyApplicationAsync(
+ Expression> predicate,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (userInfo.IsNullOrDefault())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted;
+ if (!userInfo.IsAdmin())
+ {
+ _predicate = x =>
+ x.OwnerId == userInfo.UserId &&
+ (!ignoreSoftDeleteds || !x.Deleted);
+ }
+
+ //
+ var queryable = GetApplicationsQeryable(
+ orderBuilder: null,
+ predicate: _predicate,
+ includeBuilder: includeBuilder
+ );
+
+ //
+ var entities = (await queryable
+ .ToListAsync(cancellationToken))
+ .Where(x => predicate.Compile()(x));
+
+ //
+ var result = await EnrichRangeAsync(
+ sources: entities,
+ cancellationToken: cancellationToken
+ );
+ return result;
+ }
+
+ ///
+ /// Retrieve Items based on XQuery Pagination Structure ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> QueryApplicationAsync(
+ XQuery query,
+ bool ignoreSoftDeleteds = true,
+ XUserClaimsInfoDto userInfo = null,
+ Expression> predicate = null,
+ Func, IOrderedQueryable> orderBuilder = null,
+ Func, IIncludableQueryable> includeBuilder = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Normalize Query ...
+ query = NormalizeQuery(query, configuration);
+
+ //
+ var items = await GetAllApplicationAsync(
+ userInfo: userInfo,
+ orderBuilder: orderBuilder,
+ includeBuilder: includeBuilder,
+ cancellationToken: cancellationToken,
+ ignoreSoftDeleteds: ignoreSoftDeleteds
+ );
+ var totalItemsCount = items.Count();
+
+ //
+ // Apply Filter ...
+ if (!query.Filter.IsNullOrEmpty())
+ {
+ //
+ items = items
+ .ApplyFilter(query.Filter);
+ }
+ int filteredItemsCount = items.Count();
+
+ //
+ // Count Pages ...
+ var totalPagesCount = query.CountPages(totalItemsCount);
+ var filteredPagesCount = query.CountPages(filteredItemsCount);
+
+ //
+ // Apply Paging and Sorting ...
+ if (totalItemsCount > 0 &&
+ filteredItemsCount > 0
+ )
+ {
+ //
+ // Apply Sorting ...
+ items = items
+ .ToList()
+ .ApplySorting(
+ query.SortBy,
+ query.IsAscending
+ );
+
+ //
+ // Apply Paging ...
+ items = items
+ .ToList()
+ .ApplyPaging(
+ query.Page,
+ query.PageSize
+ );
+ }
+
+ //
+ // Prepare Result ...
+ var result = new XQueryResult
+ {
+ Page = query.Page,
+ Items = [.. items],
+ PageSize = query.PageSize,
+ TotalPages = totalPagesCount,
+ TotalItems = totalItemsCount,
+ TotalFilteredPages = filteredPagesCount,
+ TotalFilteredItems = filteredItemsCount
+ };
+
+ //
+ return result;
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Providers/XApplicationProvider/XApplicationProvider+Custom.cs b/Providers/XApplicationProvider/XApplicationProvider+Custom.cs
new file mode 100644
index 0000000..7725ea0
--- /dev/null
+++ b/Providers/XApplicationProvider/XApplicationProvider+Custom.cs
@@ -0,0 +1,155 @@
+using System;
+using System.Linq;
+using System.Threading;
+using xCommons.Extensions;
+using xIdentityModels.Dtos;
+using xExceptions.Constants;
+using System.Threading.Tasks;
+using xIdentityModels.Models;
+using System.Collections.Generic;
+
+namespace xIds.Providers
+{
+ public partial class XApplicationProvider
+ {
+ //
+ #region Custom Actions ...
+ ///
+ /// Generate a New ApiKey and Add It to DB ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task CreateApiKey(
+ int applicationId,
+ TimeSpan? expiration = null,
+ IEnumerable scopes = null,
+ IEnumerable allowedIPs = null,
+ int? rateLimit = null,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //
+ // Validate ...
+ if (
+ userInfo.IsNullOrDefault() ||
+ !applicationId.IsValidIntId()
+ )
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ //
+ // Check Application Exists ...
+ var application = await GetApplicationAsync(
+ id: applicationId,
+ userInfo: userInfo,
+ includeBuilder: null,
+ ignoreSoftDeleteds: true,
+ cancellationToken: cancellationToken
+ );
+ if (application.IsNullOrDefault())
+ {
+ XException.NotFound.Throw();
+ }
+
+ //
+ // Validate Owner ...
+ if (!IsOwned(application, userInfo))
+ {
+ XException.NotAllowed.Throw();
+ }
+
+ //
+ // Normalizing ...
+ if (!rateLimit.HasValue)
+ {
+ rateLimit = apiKeyConfiguration.DefaultRateLimit;
+ }
+ if (!expiration.HasValue)
+ {
+ expiration = TimeSpan.FromMinutes(apiKeyConfiguration.DefaultExpirationMinutes);
+ }
+ else if (expiration.Value > TimeSpan.FromMinutes(apiKeyConfiguration.MaxExpirationMinutes))
+ {
+ expiration = TimeSpan.FromMinutes(apiKeyConfiguration.MaxExpirationMinutes);
+ }
+
+ //
+ var
+
+ //
+ var item = new XApiKeyDto
+ {
+
+ }
+ }
+
+ ///
+ /// Revoke Specified Api Key ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task RevokeApiKey(
+ Guid apiKeyId,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Renew Specified Api Key ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task RotateApiKey(
+ Guid apiKeyId,
+ TimeSpan? newExpiration = null,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Retrieve All Api Keys for Specified Application ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetApplicationApiKeys(
+ Guid applicationId,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Validate Specified Api Key in based on ClientIP ...
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task ValidateApiKey(
+ string apiKey,
+ string clientIP,
+ XUserClaimsInfoDto userInfo = null,
+ CancellationToken cancellationToken = default
+ )
+ { }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Providers/XApplicationProvider/XApplicationProvider+Enricher.cs b/Providers/XApplicationProvider/XApplicationProvider+Enricher.cs
new file mode 100644
index 0000000..6447457
--- /dev/null
+++ b/Providers/XApplicationProvider/XApplicationProvider+Enricher.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using xIdentityModels.Dtos;
+using xIdentityModels.Models;
+
+namespace xIds.Providers
+{
+ public partial class XApplicationProvider
+ {
+ //
+ #region Enrichers ...
+ ///
+ /// Enrich Api Key ...
+ ///
+ ///
+ ///
+ ///
+ public async Task EnrichAsync(
+ XApiKey source,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich Api Key ...
+ ///
+ ///
+ ///
+ ///
+ public async Task EnrichAsync(
+ XApiKeyDto source,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich Api Key Usage ...
+ ///
+ ///
+ ///
+ ///
+ public async Task EnrichAsync(
+ XApiKeyUsage source,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich Application ...
+ ///
+ ///
+ ///
+ ///
+ public async Task EnrichAsync(
+ XApplication source,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich Api Key Usage ...
+ ///
+ ///
+ ///
+ ///
+ public async Task EnrichAsync(
+ XApiKeyUsageDto source,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich Application ...
+ ///
+ ///
+ ///
+ ///
+ public async Task EnrichAsync(
+ XApplicationDto source,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich a Collection of Api Key(s) ...
+ ///
+ ///
+ ///
+ ///
+ public async Task> EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich a Collection of Api Key(s) ...
+ ///
+ ///
+ ///
+ ///
+ public async Task> EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich a Collection of Api Key Usage(s) ...
+ ///
+ ///
+ ///
+ ///
+ public async Task> EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich a Collection of Application(s) ...
+ ///
+ ///
+ ///
+ ///
+ public async Task> EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich a Collection of Api Key Usage(s) ...
+ ///
+ ///
+ ///
+ ///
+ public async Task> EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ )
+ { }
+
+ ///
+ /// Enrich a Collection of Application(s) ...
+ ///
+ ///
+ ///
+ ///
+ public async Task> EnrichRangeAsync(
+ IEnumerable sources,
+ CancellationToken cancellationToken = default
+ )
+ { }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/appsettings.Development.json b/appsettings.Development.json
index d894880..267897f 100644
--- a/appsettings.Development.json
+++ b/appsettings.Development.json
@@ -36,6 +36,13 @@
"ThumbQuality": 72,
"MaxFileSize": 41943040
},
+ "ApiKeyConfiguration": {
+ "DefaultRateLimit": 60,
+ "EnableAuditLog": false,
+ "MaxKeysPerApplication": "10",
+ "MaxExpirationMinutes": "525600",
+ "DefaultExpirationMinutes": "43200"
+ },
"DbSeeder": {
"UpdateExists": false,
"Clients": [