commit ff4d0d5dcb8888d94c8a22a405bdb73ceaea97b9 Author: Hadi Khazaee Asl Date: Thu Jan 25 04:41:17 2024 +0330 Initial Commit ... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bd9d0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# +# DotNet ... +bin +obj + +# +# Natural Docs ... +Documentation/* diff --git a/Configuration/XDataServiceConfiguration.cs b/Configuration/XDataServiceConfiguration.cs new file mode 100644 index 0000000..3bbfe0d --- /dev/null +++ b/Configuration/XDataServiceConfiguration.cs @@ -0,0 +1,82 @@ +using xDataService.Constants; + +namespace xDataService.Configuration { + /// + /// Represent Configurations of DataService Module ... + /// + public partial class XDataServiceConfiguration { + /// + /// Provider Type ... + /// + /// + public XDbProviders Provider { get; set; } + + /// + /// Enable Soft Delete Entities or Not ... + /// + /// + public bool EnableSoftDelete { get; set; } + + /// + /// Connection String which provide Requires Data to Connect to Db Provider ... + /// + /// + public string ConnectionString { get; set; } + + /// + /// Enable Tracking of Entities ... + /// Only Used on EFCore ... + /// + /// + public bool EnableTracking { get; set; } = false; + + /// + /// Enable Logging Details of Errors ... + /// Only Used on EFCore ... + /// + /// + public bool EnableDetailedErrors { get; set; } = false; + + /// + /// Enable Logging Sensitive Data ... + /// Only Used on EFCore ... + /// + /// + public bool EnableSensitiveDataLogging { get; set; } = false; + + /// + /// this is a way to provide Default Pagination Data on XQuery based requests ... + /// + /// + public PagingConfiguration PagingConfiguration { get; set; } = new PagingConfiguration (); + + /// + /// the base path for providing GraphQL ... + /// + /// + public string GraphQLBasePath { get; set; } = "/graphql"; + } + + /// + /// this is a way to provide Default Pagination Data on XQuery based requests ... + /// + public partial class PagingConfiguration { + /// + /// Default Page Size ... + /// + /// + public int DefaultPageSize { get; set; } = XDataServiceConstants.DEFAULT_PAGE_SIZE; + + /// + /// restrict Maximum Page Size ... + /// + /// + public int MaxAvailablePageSize { get; set; } = XDataServiceConstants.MAX_AVAILABLE_PAGE_SIZE; + + /// + /// restrice Minimum Page Size ... + /// + /// + public int MinAvailablePageSize { get; set; } = XDataServiceConstants.MIN_AVAILABLE_PAGE_SIZE; + } +} \ No newline at end of file diff --git a/Configuration/XDbProviderConfigurations.cs b/Configuration/XDbProviderConfigurations.cs new file mode 100644 index 0000000..f086a29 --- /dev/null +++ b/Configuration/XDbProviderConfigurations.cs @@ -0,0 +1,8 @@ +namespace xDataService.Configuration { + public partial class XDbProviderConfigurations { + /// + /// Default ConnectionString Name ... + /// + public const string DEFAULT_CONNECTION_NAME = "DataConnection"; + } +} \ No newline at end of file diff --git a/Constants/ConfigurationNodeNames.cs b/Constants/ConfigurationNodeNames.cs new file mode 100644 index 0000000..9f6bf23 --- /dev/null +++ b/Constants/ConfigurationNodeNames.cs @@ -0,0 +1,7 @@ +namespace xDataService.Constants { + public partial struct ConfigurationNodeNames { + public const string DB_PROVIDER_NODE = "XDBProvider"; + public const string DATA_SERVICE_NODE = "DataServiceConfiguration"; + public const string DATA_SERVICE_DB_SEED_NODE = "DbSeeder"; + } +} \ No newline at end of file diff --git a/Constants/XDataServiceConstants.cs b/Constants/XDataServiceConstants.cs new file mode 100644 index 0000000..d2ed5ed --- /dev/null +++ b/Constants/XDataServiceConstants.cs @@ -0,0 +1,10 @@ +namespace xDataService.Constants { + /// + /// Default Pagination Values ... + /// + public partial struct XDataServiceConstants { + public static int DEFAULT_PAGE_SIZE = 50; + public static int MAX_AVAILABLE_PAGE_SIZE = 500; + public static int MIN_AVAILABLE_PAGE_SIZE = 20; + } +} \ No newline at end of file diff --git a/Constants/XDbProviders.cs b/Constants/XDbProviders.cs new file mode 100644 index 0000000..91f059a --- /dev/null +++ b/Constants/XDbProviders.cs @@ -0,0 +1,23 @@ +namespace xDataService.Constants { + /// + /// Represent Supported DBMS for Managing Data ... + /// + public enum XDbProviders { + None, + MySQL, + SQLite, + SQLServer, + MongoDB, + } + + /// + /// Represent Supported DBMS for Managing Data ... + /// + public partial struct ProviderType { + public const string MySQL = "MYSQL"; + public const string SQLite = "SQLITE"; + public const string SQLServer = "SQLSERVER"; + public const string MongoDB = "MONGODB"; + } + +} \ No newline at end of file diff --git a/Controllers/XBaseEntityController.cs b/Controllers/XBaseEntityController.cs new file mode 100644 index 0000000..ae8ac76 --- /dev/null +++ b/Controllers/XBaseEntityController.cs @@ -0,0 +1,480 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Extensions; +using xCommons.Providers; +using xModels.Interfaces; +using xExceptions.Constants; +using xModels.Base; +using xModels.Dtos; +using xDataService.Interfaces; + +namespace xDataService.Controllers { + public abstract class XBaseEntityController : XBaseController, IXEntityControllerActions + where TEntity : XBaseEntity { + public readonly IXBaseRepository repository; + + protected XBaseEntityController ( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider, + IXBaseRepository repository + ) : base ( + logger, + appConfiguration, + validationProvider + ) { + // + this.repository = repository; + } + + // + #region Interface Implementations ... + // + #region Retrieve ... + [HttpGet ("{id}")] + public virtual async Task> Get ( + [FromRoute] TKey id, [FromQuery] bool ignoreSoftDeleteds = true, [FromQuery] bool containsDetail = false + ) { + // + try { + // + // Validate Args ... + ValidationProvider.NotNull (id); + + // + // Get Result ... + var result = await repository + .GetAsync ( + id, + ignoreSoftDeleteds : ignoreSoftDeleteds, + containsDetail : containsDetail + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + [HttpGet] + public virtual async Task>> GetAll ( + [FromQuery] bool ignoreSoftDeleteds = true, [FromQuery] bool containsDetail = false + ) { + // + try { + // + // Get Result ... + var result = await repository + .GetAllAsync ( + ignoreSoftDeleteds: ignoreSoftDeleteds, + containsDetail: containsDetail + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + [HttpGet ("FindOne/{query}")] + public virtual async Task> FindOne ( + [FromRoute] string query, [FromQuery] bool ignoreSoftDeleteds = true, [FromQuery] bool containsDetail = false + ) { + // + try { + // + // Validate Args ... + ValidationProvider.NotEmpty (query); + + // + // Get Result ... + var result = await repository + .FindOneAsync (t => + t.PropValuesContains (query), + ignoreSoftDeleteds : ignoreSoftDeleteds, + containsDetail : containsDetail + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + [HttpGet ("FindMany/{query}")] + public virtual async Task>> FindMany ( + [FromRoute] string query, [FromQuery] bool ignoreSoftDeleteds = true, [FromQuery] bool containsDetail = false + ) { + // + try { + // + // Validate Args ... + ValidationProvider.NotEmpty (query); + + // + // Get Result ... + var result = await repository + .FindManyAsync (t => + t.PropValuesContains (query), + ignoreSoftDeleteds : ignoreSoftDeleteds, + containsDetail : containsDetail + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + [HttpGet ("Query")] + public virtual async Task>> Query ( + [FromQuery] XQuery query, [FromQuery] bool ignoreSoftDeleteds = true + ) { + // + try { + // + // Validate Args ... + ValidationProvider.NotNull (query); + + // + // Get Result ... + var result = await repository + .QueryAsync ( + query, + ignoreSoftDeleteds : ignoreSoftDeleteds + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + // + // TODO: Fix this ... + // [HttpGet ("RequestPage")] + // public virtual async Task>> RequestPage ( + // [FromQuery] XPageRequest request, [FromQuery] bool ignoreSoftDeleteds = true, [FromQuery] bool containsDetail = false + // ) { + // // + // try { + // // + // // Validate Args ... + // ValidationProvider.NotNull (request); + + // // + // // Get Result ... + // var result = await repository + // .RequestPageAsync ( + // request, + // ignoreSoftDeleteds : ignoreSoftDeleteds, + // containsDetail : containsDetail + // ); + + // // + // return Ok (result + // .ToDynamicObject ()); + // } catch (Exception ex) { + // // + // var result = GetExceptionActionResult (ex); + // return result; + // } + // } + #endregion + + // + #region Add ... + [HttpPost] + public virtual async Task> Add ( + [FromBody] TEntity item + ) { + // + try { + // + // Validate Args ... + if (!ModelState.IsValid) { + XException.InvalidArgs.Throw (); + } + ValidationProvider.NotNull (item); + + // + // Get Result ... + var result = await repository + .AddAsync ( + item, + saveChanges : true + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + [HttpPost ("AddOrUpdate")] + public virtual async Task> AddOrUpdate ( + [FromBody] TEntity item + ) { + // + try { + // + // Validate Args ... + if (!ModelState.IsValid) { + XException.InvalidArgs.Throw (); + } + ValidationProvider.NotNull (item); + + // + // Get Result ... + var result = await repository + .AddOrUpdateAsync ( + item, + saveChanges : true + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + [HttpPost ("AddMany")] + public virtual async Task AddMany ( + [FromBody] XBaseRangeRequest request + ) { + // + try { + // + // Validate Args ... + if (!ModelState.IsValid) { + XException.InvalidArgs.Throw (); + } + await ValidationProvider + .GroupValidationBuilder () + .AddNotNull (request) + .AddNotZeroChilds (request.Items) + .ValidateGroupAsync (); + + // + // Get Result ... + await repository + .AddRangeAsync ( + request.Items, + saveChanges : true + ); + + // + return Ok (); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + #endregion + + // + #region Update ... + [HttpPut ("{id}")] + public virtual async Task> Update ( + [FromRoute] TKey id, [FromBody] TEntity item + ) { + // + try { + // + // Validate Args ... + if (!ModelState.IsValid) { + XException.InvalidArgs.Throw (); + } + await ValidationProvider + .GroupValidationBuilder () + .AddNotNull (id, item) + .ValidateGroupAsync (); + + // + // Get Result ... + var result = await repository + .UpdateAsync ( + id, + item, + saveChanges : true + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + [HttpPost ("UpdateMany")] + public virtual async Task> UpdateMany ( + [FromBody] XBaseRangeRequest request + ) { + // + try { + // + // Validate Args ... + if (!ModelState.IsValid) { + XException.InvalidArgs.Throw (); + } + await ValidationProvider + .GroupValidationBuilder () + .AddNotNull (request) + .AddNotZeroChilds (request.Items) + .ValidateGroupAsync (); + + // + // Get Result ... + var result = await repository + .UpdateRangeAsync ( + request.Items, + saveChanges : true + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + #endregion + + // + #region Exists ... + [HttpGet ("{id}/IsExists")] + public virtual async Task> IsExists ( + [FromRoute] TKey id, [FromQuery] bool ignoreSoftDeleteds = true + ) { + // + try { + // + // Validate Args ... + ValidationProvider.NotNull (id); + + // + // Get Result ... + var result = await repository + .IsExistsAsync ( + id, + ignoreSoftDeleteds : ignoreSoftDeleteds + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + #endregion + + // + #region Remove ... + [HttpDelete ("{id}")] + public virtual async Task> Remove ( + [FromRoute] TKey id, + bool softDelete = true + ) { + // + try { + // + // Validate Args ... + ValidationProvider.NotNull (id); + + // + // Get Result ... + var result = await repository + .RemoveAsync ( + id, + saveChanges : true, + softDelete : softDelete + ); + + // + return Ok (result + .ToDynamicObject ()); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + + [HttpPost ("RemoveMany")] + public virtual async Task RemoveMany ( + [FromBody] XBaseRangeRequest request, + bool softDelete = true + ) { + // + try { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder () + .AddNotNull (request) + .AddNotZeroChilds (request.Items) + .ValidateGroupAsync (); + + // + // Get Result ... + await repository + .RemoveRangeAsync ( + request.Items, + saveChanges : true, + softDelete : softDelete + ); + + // + return Ok (); + } catch (Exception ex) { + // + var result = GetExceptionActionResult (ex); + return result; + } + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/DI/XDIHelperExtension.cs b/DI/XDIHelperExtension.cs new file mode 100644 index 0000000..b3ad6f3 --- /dev/null +++ b/DI/XDIHelperExtension.cs @@ -0,0 +1,622 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Server; +using GraphQL.Server.Ui.Playground; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using MySql.EntityFrameworkCore.Extensions; +using xCommons.Extensions; +using xDataService.Configuration; +using xDataService.Constants; +using xDataService.Db; +using xDataService.Extensions; +using xDataService.Interfaces; +using xExceptions.Constants; +using xModels.Base; + +namespace xDataService.DI { + public static class XDIHelperExtension { + /// + /// Extract Connection String From IConfiguration + /// + /// + /// + /// + public static string GetXConnectionString ( + this IConfiguration config, + string connectionName = null + ) { + // + if (connectionName.IsNullOrEmpty ()) { + connectionName = XDbProviderConfigurations.DEFAULT_CONNECTION_NAME; + } + + // + return config.GetConnectionString (connectionName); + } + + /// + /// Retrieve Data Provider Type from Configurations + /// + /// + /// + public static XDbProviders GetXDbProviderType (this IConfiguration source) { + // + var provider = (source[$"{ConfigurationNodeNames.DB_PROVIDER_NODE}"]) + .ToNormalString (); + + // + return provider.ToDbProvider (); + } + + /// + /// Retrive XDataService Configurations + /// + /// + /// + /// + public static XDataServiceConfiguration GetXDataServiceConfiguration ( + this IConfiguration source, + string connectionName = null + ) { + // + var xDataServiceConfigSection = source + .GetSection (ConfigurationNodeNames.DATA_SERVICE_NODE); + var result = xDataServiceConfigSection.Get (); + + // + result.Provider = source.GetXDbProviderType (); + result.ConnectionString = source.GetXConnectionString (connectionName); + + // + return result; + } + + /// + /// Register XDataService Configuration + /// + /// + /// + /// + public static void AddXDataServiceConfiguration ( + this IServiceCollection services, + IConfiguration configuration, + string connectionName = null + ) { + // + var dataServiceConfiguration = configuration + .GetXDataServiceConfiguration (connectionName); + if (dataServiceConfiguration.IsNull ()) { + dataServiceConfiguration = new XDataServiceConfiguration (); + } + + // + services.AddSingleton (dataServiceConfiguration); + } + + /// + /// Register XDataService Configuration + /// + /// + /// + public static void AddXDataServiceConfiguration ( + this IServiceCollection services, + XDataServiceConfiguration configuration + ) { + // + if (configuration.IsNull ()) { + configuration = new XDataServiceConfiguration (); + } + + // + services.AddSingleton (configuration); + } + + /// + /// Register XDataService on DI + /// Only Used when EFCore Provider configured to use ... + /// + /// + /// + /// + /// + /// + public static void AddXDataService ( + this IServiceCollection services, + IConfiguration config, + ServiceLifetime lifetime, + string connectionName = null, + Action optionsBuilder = null + ) + where TDbContext : XDbContext + where TDbSeeder : IXDbSeeder { + // + // Register DataService Configuration ... + if (services.GetRegisteredService ().IsNull ()) { + Console.WriteLine ($"XDataService: there is no provided DataService Config, try to register default ..."); + services.AddXDataServiceConfiguration (config, connectionName); + } + + // + // prevent from going forward if there is no Provider configured ... + var providerType = config.GetXDbProviderType (); + Console.WriteLine ($"XDataService: registered db provider type is => {providerType} ..."); + if (providerType == XDbProviders.None) { + return; + } + + // + // Retrieve IXDataServiceHelper ... + var dataServiceHelper = services.GetRegisteredService (); + if (!dataServiceHelper.IsNull ()) { + // + // Use Db Context Helper ... + Console.WriteLine ($"XDataService: use provided IXDbContextHelper ..."); + + // + // Retrieve Connection String ... + var addContext = true; + var connectionString = config.GetXConnectionString (connectionName); + switch (providerType) { + // + case XDbProviders.MySQL: + addContext = true; + services.AddEntityFrameworkMySQL (); + break; + + // + case XDbProviders.SQLite: + addContext = true; + services.AddEntityFrameworkSqlite (); + break; + + // + case XDbProviders.SQLServer: + addContext = true; + services.AddEntityFrameworkSqlServer (); + break; + + // + case XDbProviders.MongoDB: + addContext = false; + break; + } + + // + // Register DbContext in DI ... + if (addContext) { + dataServiceHelper.AddDbContext ( + services, + connectionString, + providerType, + lifetime, + optionsBuilder + ); + + // + // Register Unit Of Works ... + services.Add (new ServiceDescriptor (typeof (IXUnitOfWorks), typeof (XUnitOfWorks), lifetime)); + } + + // + // Comment this in related to Task no. 27 ... + // Register IXSequentialGuid for handle XBaseGuidEntities ... + // services.AddSingleton (); + + // + // Register All Repository Patterns ... + AddRepositories (services, config, lifetime, providerType); + } else { + Console.WriteLine ($"XDataService: there is no provided IXDbContextHelper, data service registration failed ..."); + } + } + + /// + /// Register XDataService on DI + /// Only Used when non EFCore Provider configured to use ... + /// + /// + /// + /// + /// + public static void AddXDataService ( + this IServiceCollection services, + IConfiguration config, + ServiceLifetime lifetime, + string connectionName = null + ) + where TDbSeeder : IXDbSeeder { + // + // Register DataService Configuration ... + if (services.GetRegisteredService ().IsNull ()) { + Console.WriteLine ($"XDataService: there is no provided DataService Config, try to register default ..."); + services.AddXDataServiceConfiguration (config, connectionName); + } + + // + // prevent from going forward if there is no Provider configured ... + var providerType = config.GetXDbProviderType (); + Console.WriteLine ($"XDataService: registered db provider type is => {providerType} ..."); + if (providerType == XDbProviders.None) { + return; + } + + // + // Retrieve IXDataServiceHelper ... + var dataServiceHelper = services.GetRegisteredService (); + if (!dataServiceHelper.IsNull ()) { + // + // Use Db Context Helper ... + Console.WriteLine ($"XDataService: use provided IXDataServiceHelper ..."); + + // + // Retrieve Connection String ... + var connectionString = config.GetXConnectionString (connectionName); + switch (providerType) { + // + case XDbProviders.MySQL: + case XDbProviders.SQLite: + case XDbProviders.SQLServer: + Console.WriteLine ($"XDataService: for EF Provider Types use AddXDataService instead of AddXDataService ..."); + XException.InvalidConfiguration.Throw (); + break; + + // + case XDbProviders.MongoDB: + // + #region XBaseEntity Mappers ... + // + // Map ID as BsonId here ... + BsonClassMap.RegisterClassMap> (cm => { + // + cm.AutoMap (); + cm.MapIdMember (c => c.Id); + }); + BsonClassMap.RegisterClassMap> (cm => { + // + cm.AutoMap (); + cm.MapIdMember (c => c.Id); + }); + BsonClassMap.RegisterClassMap> (cm => { + // + cm.AutoMap (); + cm.MapIdMember (c => c.Id); + }); + #endregion + + // + var conventionPacks = dataServiceHelper.MongoConventionPacks (); + if (!conventionPacks.IsNull ()) { + ConventionRegistry.Register ( + nameof ( + dataServiceHelper.MongoConventionPacks + ), + conventionPacks, + type => !type.FullName + .IsNullOrEmpty () + ); + } + + // + dataServiceHelper.RegisterMongoExtras (); + break; + } + + // + // Comment this in related to Task no. 27 ... + // Register IXSequentialGuid for handle XBaseGuidEntities ... + // services.AddSingleton (); + + // + // Register All Repository Patterns ... + AddRepositories (services, config, lifetime, providerType); + } else { + Console.WriteLine ($"XDataService: there is no provided IXDbContextHelper, data service registration failed ..."); + } + } + + /// + /// Register XGraphQL service ... + /// + /// + /// + public static void AddXGraphQL ( + this IServiceCollection services, + Action options = null + ) { + // + var xGraphQLHelper = services.GetRegisteredService (); + if (!xGraphQLHelper.IsNull ()) { + // + xGraphQLHelper.AddXGraphEnumTypes (services); + xGraphQLHelper.AddXGraphObjectTypes (services); + xGraphQLHelper.AddXGraphInputTypes (services); + xGraphQLHelper.AddXGraphQueries (services); + xGraphQLHelper.AddXGraphMutations (services); + xGraphQLHelper.AddXGraphSubscriptions (services); + xGraphQLHelper.AddXGraphSchemas (services); + + // + // Add GraphQL Server ... + if (options.IsNull ()) { + // + options = x => { }; + Console.WriteLine ($"XDataService: there is no provided GraphQLOption, use default ..."); + } + + // + var xGraphQlBuilder = services.AddGraphQL (options) + .AddNewtonsoftJson (deserializerSettings => { }, serializerSettings => { }) + .AddWebSockets () + .AddDataLoader () + .AddGraphTypes (); + + // + xGraphQLHelper.AddXGraphTypes (xGraphQlBuilder); + } else { + Console.WriteLine ($"XDataService: there is no provided IXGraphQLHelper, data service registration failed ..."); + } + } + + /// + /// Use XDataService MiddleWare + /// + /// + public static void UseXDataService ( + this IApplicationBuilder app + ) { + app.SeedData (); + } + + /// + /// Use XGraphQL Middleware ... + /// + /// + /// + /// + public static void UseXGraphQL ( + this IApplicationBuilder app, + GraphQLPlaygroundOptions options = null, + bool withPlayground = true + ) { + // + // Use GraphQl WebSockets ... + app.UseWebSockets (); + + // + // Registered Graph Types ... + using (var scope = app.ApplicationServices.CreateScope ()) { + // + var xGraphQLHelper = scope.ServiceProvider.GetService (); + if (!xGraphQLHelper.IsNull ()) { + xGraphQLHelper.UseXGraph (app); + } else { + Console.WriteLine ($"XDataService: there is no provided IXGraphQLHelper, data service GraphQL not using ..."); + } + } + + // + // Use GraphQL Playground UI ... + if (withPlayground) { + app.UseGraphQLPlayground (options); + } + } + + // + #region Private ... + /// + /// Register Repositories on DI as Services + /// + /// + /// + /// + /// + private static void AddRepositories ( + IServiceCollection services, + IConfiguration config, + ServiceLifetime lifetime, + XDbProviders provider + ) + where TDbContext : XDbContext + where TDbSeeder : IXDbSeeder { + // + // Add Repositories Based On Provider Here ... + switch (provider) { + case XDbProviders.MySQL: + case XDbProviders.SQLite: + case XDbProviders.SQLServer: + AddEFRepositories (services, lifetime); + break; + case XDbProviders.MongoDB: + Console.WriteLine ($"XDataService: for Mongo Provider Types use AddXDataService instead of AddXDataService ..."); + XException.InvalidConfiguration.Throw (); + break; + } + + // + // Retrieve IXDataServiceHelper ... + var dataServiceHelper = services.GetRegisteredService (); + if (dataServiceHelper.IsNull ()) { + // + Console.WriteLine ($"XDataService: there is no provided IXDataServiceHelper, db seeder registration failed ..."); + return; + } + + // + // Add DbSeeder Configuration ... + dataServiceHelper.AddDbSeederConfiguration (services, config); + + // + // Add Db Seeder ... + dataServiceHelper.AddDbSeeder (services, config, lifetime); + } + + /// + /// Register Repositories on DI as Services + /// + /// + /// + /// + /// + private static void AddRepositories ( + IServiceCollection services, + IConfiguration config, + ServiceLifetime lifetime, + XDbProviders provider + ) + where TDbSeeder : IXDbSeeder { + // + // Add Repositories Based On Provider Here ... + switch (provider) { + // + case XDbProviders.MySQL: + case XDbProviders.SQLite: + case XDbProviders.SQLServer: + Console.WriteLine ($"XDataService: for EF Provider Types use AddXDataService instead of AddXDataService ..."); + XException.InvalidConfiguration.Throw (); + break; + + // + case XDbProviders.MongoDB: + AddMongoRepositories (services, lifetime); + break; + } + + // + // Retrieve IXDataServiceHelper ... + var dataServiceHelper = services.GetRegisteredService (); + if (dataServiceHelper.IsNull ()) { + // + Console.WriteLine ($"XDataService: there is no provided IXDataServiceHelper, db seeder registration failed ..."); + return; + } + + // + // Add DbSeeder Configuration ... + dataServiceHelper.AddDbSeederConfiguration (services, config); + + // + // Add Db Seeder ... + dataServiceHelper.AddDbSeeder (services, config, lifetime); + } + + /// + /// Add Entity Framework Repositories + /// + /// + /// + private static void AddEFRepositories ( + this IServiceCollection services, + ServiceLifetime lifetime + ) + where TDbContext : XDbContext { + // + var repoServices = new List (); + + // + var dataServiceConfig = services.GetRegisteredService (); + if (!dataServiceConfig.IsNull ()) { + // + // Register Base Repositories if Required ... + } + + // + // Adding Services to DI ... + if (repoServices.Count > 0) { + repoServices + .ToList () + .ForEach (s => services.Add (s)); + } + + // + // Retrieve IXDataServiceHelper ... + var dataServiceHelper = services.GetRegisteredService (); + if (!dataServiceHelper.IsNull ()) { + // + // Register Repositories ... + dataServiceHelper.AddRepositories (services, lifetime); + + // + // Register KeyGenerators ... + dataServiceHelper.AddKeyGenerators (services); + } + } + + /// + /// Add MongoDb Repositories + /// + /// + /// + private static void AddMongoRepositories ( + this IServiceCollection services, + ServiceLifetime lifetime + ) { + // + var repoServices = new List (); + + // + var dataServiceConfig = services.GetRegisteredService (); + if (!dataServiceConfig.IsNull ()) { + // + // Register Base Repositories if Required ... + } + + // + // Adding Services to DI ... + if (repoServices.Count > 0) { + repoServices + .ToList () + .ForEach (s => services.Add (s)); + } + + // + // Retrieve IXDataServiceHelper ... + var dataServiceHelper = services.GetRegisteredService (); + if (!dataServiceHelper.IsNull ()) { + // + // Register Repositories ... + dataServiceHelper.AddRepositories (services, lifetime); + + // + // Register KeyGenerators ... + dataServiceHelper.AddKeyGenerators (services); + } + } + + /// + /// Do Seeding Initialization Data on DbContext + /// + /// + private static void SeedData ( + this IApplicationBuilder app + ) { + // + using (var scope = app.ApplicationServices.CreateScope ()) { + // + var dbSeeder = scope.ServiceProvider.GetService (); + Console.WriteLine ($"XDataService: dbSeeder retrieved from DI is => {!dbSeeder.IsNull()}"); + + // + if (!dbSeeder.IsNull ()) { + try { + // + dbSeeder.Seed () + .GetAwaiter () + .GetResult (); + } catch (Exception ex) { + // + Console.WriteLine (" "); + Console.WriteLine ($"XDataService: Seeding Error: {ex.Message}"); + XException.InvalidConfiguration.Throw (); + } + } + } + } + #endregion + } +} \ No newline at end of file diff --git a/Db/XDbContext.cs b/Db/XDbContext.cs new file mode 100644 index 0000000..f9e62b3 --- /dev/null +++ b/Db/XDbContext.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore; +using xDataService.Configuration; + +namespace xDataService.Db { + /// + /// Provide an abstraction layout arround DBContext ... + /// Only Used on EFCore ... + /// + public abstract class XDbContext : DbContext { + private readonly XDataServiceConfiguration config; + + // + #region Entities ... + // + // Register Default and Base Entities ... + #endregion + + // + // Constructor ... + public XDbContext ( + DbContextOptions options, + XDataServiceConfiguration config + ) : base (options) { + // + try { + Database.EnsureCreated (); + Database.Migrate (); + } catch { } + + this.config = config; + } + + // + // Prepare Fluent API ... + protected override void OnModelCreating (ModelBuilder modelBuilder) { + // + OnXModelCreating (modelBuilder); + + // + // Since this class inherit from Identity Db Context, + // we have to pass this to Building Identity Models ... + base.OnModelCreating (modelBuilder); + } + + // + public abstract void OnXModelCreating (ModelBuilder modelBuilder); + + // + // Prepare Configuring ... + protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder) { + // + if (config.EnableDetailedErrors) { + optionsBuilder.EnableDetailedErrors (); + } + + // + if (config.EnableSensitiveDataLogging) { + optionsBuilder.EnableSensitiveDataLogging (); + } + + // + if (config.EnableTracking) { + optionsBuilder.UseQueryTrackingBehavior (QueryTrackingBehavior.TrackAll); + } else { + optionsBuilder.UseQueryTrackingBehavior (QueryTrackingBehavior.NoTracking); + } + + // + OnXConfiguring (optionsBuilder); + + // + base.OnConfiguring (optionsBuilder); + } + + // + public abstract void OnXConfiguring (DbContextOptionsBuilder optionsBuilder); + } +} \ No newline at end of file diff --git a/Db/XUnitOfWorks.cs b/Db/XUnitOfWorks.cs new file mode 100644 index 0000000..ca57bea --- /dev/null +++ b/Db/XUnitOfWorks.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using xDataService.Interfaces; +using xModels.Base; + +namespace xDataService.Db { + /// + /// Provide Unit Of Works Design Pattern for Db Transactions ... + /// Only Used on EFCore ... + /// + /// + public class XUnitOfWorks : IXUnitOfWorks + where TDbContext : XDbContext { + public TDbContext DbContext { get; } + + public XUnitOfWorks (TDbContext context) { + this.DbContext = context; + } + + public DbSet GetDbSet () + where T : XBaseEntity { + return DbContext.Set (); + } + + /// + /// Save Changes + /// + public int SaveChanges () { + return DbContext.SaveChanges (); + } + + /// + /// Save Changes Async + /// + /// + public async Task SaveChangesAsync () { + return await DbContext.SaveChangesAsync (); + } + + public void Dispose () { + DbContext.Dispose (); + } + } +} \ No newline at end of file diff --git a/EFRepositories/XBaseEFRepository.cs b/EFRepositories/XBaseEFRepository.cs new file mode 100644 index 0000000..43f438e --- /dev/null +++ b/EFRepositories/XBaseEFRepository.cs @@ -0,0 +1,1093 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using xCommons.Extensions; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Extensions; +using xDataService.Interfaces; +using xDataService.Models; +using xModels.Base; +using xModels.Dtos; + +namespace xDataService.EFRepositories { + /// + /// Base EFCore Base Entity Repository Pattern implementation ... + /// Only Used on EFCore ... + /// + /// is the Entity type + /// is the Entity Key Type + /// is the DbContext Type + public abstract class XBaseEFRepository : IXBaseRepository + where T : XBaseEntity + where TDbContext : XDbContext { + // + public readonly DbSet dbSet; + private readonly IXKeyGenerator keyGenerator; + public readonly IXUnitOfWorks unitOfWorks; + public readonly XDataServiceConfiguration configuration; + private readonly IXBaseRepositoryEvents baseRepositoryEvents; + + // + public abstract IQueryable GetFullDbSet (); + + // + #region Constructor ... + public XBaseEFRepository ( + IXUnitOfWorks unitOfWorks, + XDataServiceConfiguration configuration, + IXKeyGenerator keyGenerator = null, + IXBaseRepositoryEvents baseRepositoryEvents = null + ) { + this.keyGenerator = keyGenerator; + // + this.unitOfWorks = unitOfWorks; + this.configuration = configuration; + this.dbSet = unitOfWorks.GetDbSet (); + this.baseRepositoryEvents = baseRepositoryEvents; + } + #endregion + + // + #region Add ... + public async Task AddAsync ( + T item, + bool saveChanges = true + ) { + // + // Handle Key ... + item = await HandleKeyAsync (item); + + // + var entry = await dbSet.AddAsync (item); + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .AddEvent (new XBaseEventModel (entry.Entity)); + } + + // + // Return result base on action Succeed ... + return isSucceed ? + entry.Entity : + null; + } + + public async Task AddOrUpdateAsync ( + T item, + bool saveChanges = true + ) { + // + var isExists = await IsExistsAsync (GetKey (item)); + if (!isExists) { + return await AddAsync ( + item, + saveChanges + ); + } else { + return await UpdateAsync ( + GetKey (item), + item, + saveChanges + ); + } + } + + public async Task AddRangeAsync ( + IEnumerable items, + bool saveChanges = true + ) { + // + await dbSet.AddRangeAsync ( + await Task + .WhenAll ( + items + .Select (async i => await HandleKeyAsync (i)) + ) + ); + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .AddManyEvent (new XBaseEventModel> (null)); + } + } + #endregion + + // + #region Remove ... + public async Task RemoveAsync ( + TKey id, + bool softDelete = true, + bool saveChanges = true + ) { + // + // Retrieve Item ... + var item = await GetAsync ( + id, + ignoreSoftDeleteds : false + ); + if (item.IsNull ()) { + return null; + } + + // + // Handle Remove ... + EntityEntry entry = null; + if (softDelete && configuration.EnableSoftDelete) { + // + item.Deleted = true; + entry = dbSet.Update (item); + } else { + entry = dbSet.Remove (item); + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .RemoveEvent (new XBaseEventModel (entry.Entity)); + } + + // + // Return result base on action Succeed ... + return isSucceed ? + entry.Entity : + null; + } + + public async Task RemoveAsync ( + T item, + bool softDelete = true, + bool saveChanges = true + ) { + // + // Check item Exists ... + var isExists = await IsExistsAsync ( + GetKey (item), + ignoreSoftDeleteds : false + ); + if (!isExists) { + return null; + } + + // + // Handle Remove ... + EntityEntry entry = null; + if (softDelete && configuration.EnableSoftDelete) { + // + item.Deleted = true; + entry = dbSet.Update (item); + } else { + entry = dbSet.Remove (item); + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .RemoveEvent (new XBaseEventModel (entry.Entity)); + } + + // + // Return result base on action Succeed ... + return isSucceed ? + entry.Entity : + null; + } + + public async Task RemoveRangeAsync ( + IEnumerable items, + bool softDelete = true, + bool saveChanges = true + ) { + // + // Handle Remove ... + if (softDelete && configuration.EnableSoftDelete) { + // + foreach (var item in items) { + item.Deleted = true; + } + + // + dbSet.UpdateRange (items); + } else { + dbSet.RemoveRange (items); + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .RemoveManyEvent (new XBaseEventModel> (null)); + } + } + #endregion + + // + #region Retrieve ... + public IQueryable AsQueryable () { + return dbSet.AsQueryable (); + } + + public async Task GetAsync ( + TKey id, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ) { + // + var result = await FindOneAsync (i => + GetKey (i) + .ToString () == + id + .ToString (), + ignoreSoftDeleteds : ignoreSoftDeleteds, + containsDetail : containsDetail + ); + + // + return result; + } + + public async Task> GetAllAsync ( + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ) { + return await Task.Run (() => { + // + var result = GetDbSet ( + containsDetail: containsDetail + ); + + // + // Handle Soft Deleted Items ... + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + result = result.Where (x => x.Deleted == false); + } + + // + return result.AsEnumerable (); + }); + } + + public async Task FindOneAsync ( + Expression> whereClause, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ) { + // + // Generate Where Function ... + var whereFunc = whereClause.Compile (); + + // + // Handle Soft Deleted Items ... + Func ignoreSoftDeletedsWhereFunc = null; + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + // + Expression> ignoreSoftDeletedsWhereClause = x => x.Deleted == false; + ignoreSoftDeletedsWhereFunc = ignoreSoftDeletedsWhereClause.Compile (); + } + + // + // Get Enumerable ... + var enumerator = GetDbSet ( + containsDetail: containsDetail + ) + .AsAsyncEnumerable (); + + // + T result = null; + await + foreach (var entity in enumerator) { + // + var isApproved = whereFunc (entity) && + (ignoreSoftDeletedsWhereFunc.IsNull () ? + true : + ignoreSoftDeletedsWhereFunc (entity)); + if (isApproved) { + // + result = entity; + break; + } + } + + // + return result; + } + + public async Task> FindManyAsync ( + Expression> whereClause, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ) { + // + // Generate Where Function ... + var whereFunc = whereClause.Compile (); + + // + // Handle Soft Deleted Items ... + Func ignoreSoftDeletedsWhereFunc = null; + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + // + Expression> ignoreSoftDeletedsWhereClause = x => x.Deleted == false; + ignoreSoftDeletedsWhereFunc = ignoreSoftDeletedsWhereClause.Compile (); + } + + // + // Get Enumerable ... + var enumerator = GetDbSet ( + containsDetail: containsDetail + ) + .AsAsyncEnumerable (); + + // + var result = new List (); + await + foreach (var entity in enumerator) { + // + var isApproved = whereFunc (entity) && + (ignoreSoftDeletedsWhereFunc.IsNull () ? + true : + ignoreSoftDeletedsWhereFunc (entity)); + if (isApproved) { + result.Add (entity); + } + } + + // + return result.AsEnumerable (); + } + + public async Task> QueryAsync ( + XQuery query, + bool ignoreSoftDeleteds = true + ) { + // + if (query.PageSize < configuration + .PagingConfiguration + .MinAvailablePageSize) { + query.PageSize = configuration + .PagingConfiguration + .DefaultPageSize; + } + + // + if (query.PageSize > configuration + .PagingConfiguration + .MaxAvailablePageSize) { + query.PageSize = configuration + .PagingConfiguration + .MaxAvailablePageSize; + } + + // + // Handle Soft Deleted Items ... + Func ignoreSoftDeletedsWhereFunc = null; + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + // + Expression> ignoreSoftDeletedsWhereClause = x => x.Deleted == false; + ignoreSoftDeletedsWhereFunc = ignoreSoftDeletedsWhereClause.Compile (); + } + + // + // Get Enumerable ... + var enumerator = GetDbSet ( + containsDetail: query.ContainsDetail + ) + .AsAsyncEnumerable (); + + // + var resultItems = new List (); + await + foreach (var entity in enumerator) { + // + var isApproved = (ignoreSoftDeletedsWhereFunc.IsNull () ? + true : + ignoreSoftDeletedsWhereFunc (entity)); + if (isApproved) { + resultItems.Add (entity); + } + } + + // + var result = resultItems.AsEnumerable (); + + // + // Count Total Items ... + var totalItemsCount = result.Count (); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty ()) { + result = await result + .ApplyFilterAsync (query.Filter); + } + + // + // Count Total Items ... + int totalFilteredItemsCount = result.Count (); + + // + // Apply Paging ... + result = result + .ApplyPaging ( + query.Page, + query.PageSize); + + // + // Apply Sorting ... + if (!query.SortBy.IsNullOrEmpty ()) { + result = result + .ApplySorting ( + query.SortBy, + query.IsAscending); + } + + // + // Generate Result Object ... + // Query = query, + var queryResult = new XQueryResult { + Items = result.AsEnumerable (), + Page = query.Page, + PageSize = query.PageSize, + TotalItems = totalItemsCount, + TotalPages = await PagesCountAsync ( + query.PageSize, + totalFilteredItemsCount + ), + TotalFilteredItems = totalFilteredItemsCount + }; + + // + return queryResult; + } + + public async Task> ConditionalQueryAsync ( + Expression> condition, + XQuery query, + bool ignoreSoftDeleteds = true + ) { + // + if (query.PageSize < configuration + .PagingConfiguration + .MinAvailablePageSize) { + query.PageSize = configuration + .PagingConfiguration + .DefaultPageSize; + } + + // + if (query.PageSize > configuration + .PagingConfiguration + .MaxAvailablePageSize) { + query.PageSize = configuration + .PagingConfiguration + .MaxAvailablePageSize; + } + + // + // Generate Where Function ... + var whereFunc = condition.Compile (); + + // + // Handle Soft Deleted Items ... + Func ignoreSoftDeletedsWhereFunc = null; + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + // + Expression> ignoreSoftDeletedsWhereClause = x => x.Deleted == false; + ignoreSoftDeletedsWhereFunc = ignoreSoftDeletedsWhereClause.Compile (); + } + + // + // Get Enumerable ... + var enumerator = GetDbSet ( + containsDetail: query.ContainsDetail + ) + .AsAsyncEnumerable (); + + // + var resultItems = new List (); + await + foreach (var entity in enumerator) { + // + var isApproved = whereFunc (entity) && + (ignoreSoftDeletedsWhereFunc.IsNull () ? + true : + ignoreSoftDeletedsWhereFunc (entity)); + if (isApproved) { + resultItems.Add (entity); + } + } + + // + var result = resultItems.AsQueryable (); + + // + // Count Total Items ... + var totalItemsCount = result.Count (); + + // + // Apply Filter ... + if (totalItemsCount > 0 && + !query.Filter.IsNullOrEmpty ()) { + result = result + .ApplyFilter (query.Filter); + } + + // + // Count Total Items ... + var totalFilteredItemsCount = result.Count (); + + // + // Apply Paging ... + if (totalItemsCount > 0 && + totalFilteredItemsCount > 0) { + result = result + .ApplyPaging ( + query.Page, + query.PageSize); + + // + // Apply Sorting ... + if (!query.SortBy.IsNullOrEmpty ()) { + result = result + .ApplySorting ( + query.SortBy, + query.IsAscending); + } + } + + // + // Generate Result Object ... + var queryResult = new XQueryResult { + Items = result.AsEnumerable (), + Page = query.Page, + PageSize = query.PageSize, + TotalItems = totalItemsCount, + TotalPages = await PagesCountAsync ( + query.PageSize, + totalFilteredItemsCount + ), + TotalFilteredItems = totalFilteredItemsCount + }; + + // + return queryResult; + } + + // + // TODO: Fix this ... + // public Task> RequestPageAsync ( + // XPageRequest request, + // bool ignoreSoftDeleteds = true, + // bool containsDetail = true + // ) { + // // + // var result = GetDbSet ( + // containsDetail: containsDetail + // ); + + // // + // // Handle Soft Deleted Items ... + // if (ignoreSoftDeleteds) { + // result = result.Where (x => x.Deleted == false); + // } + + // // + // if (request.First.HasValue) { + // // + // if (!request.After.IsNullOrEmpty ()) { + // // + // TKey lastId = XCursorHelper.FromCursor (request.After); + // result = result.Where (x => GetKey (x).ToString() > lastId.ToString()); + // } + + // // + // result = result.Take (request.First.Value); + // } + + // // + // // Apply Sorting ... + // List nodes = null; + // if (!request.SortBy.IsNullOrEmpty ()) { + // nodes = result + // .ApplySorting ( + // request.SortBy, !request.DescendingSort + // ).ToList (); + // } + + // // + // // Claculate Required Info ... + // int totalCount = result.CountAsync ().Result; + // int maxId = nodes.Max (x => Convert.ToInt32 (GetKey (x))); + // int minId = nodes.Min (x => Convert.ToInt32 (GetKey (x))); + // bool hasNextPage = nodes.Any (x => Convert.ToInt32 (GetKey (x)) > maxId); + // bool hasPreviousPage = nodes.Any (x => Convert.ToInt32 (GetKey (x)) < minId); + + // // + // return Task.FromResult (new XPageResponse { + // Nodes = nodes, + // TotalCount = totalCount, + // HasNextPage = hasNextPage, + // HasPreviousPage = hasPreviousPage + // }); + // } + + // + // TODO: Fix this ... + // public Task> RequestConditionalPageAsync ( + // Expression> condition, + // XPageRequest request, + // bool ignoreSoftDeleteds = true, + // bool containsDetail = true + // ) { + // // + // var result = GetDbSet ( + // containsDetail: containsDetail + // ) + // .Where (condition); + + // // + // // Handle Soft Deleted Items ... + // if (ignoreSoftDeleteds) { + // result = result.Where (x => x.Deleted == false); + // } + + // // + // if (request.First.HasValue) { + // // + // if (!request.After.IsNullOrEmpty ()) { + // // + // int lastId = XCursorHelper.FromCursor (request.After); + // result = result.Where (x => Convert.ToInt32 (GetKey (x)) > lastId); + // } + + // // + // result = result.Take (request.First.Value); + // } + + // // + // // Apply Sorting ... + // List nodes = null; + // if (!request.SortBy.IsNullOrEmpty ()) { + // nodes = result + // .ApplySorting ( + // request.SortBy, !request.DescendingSort) + // .ToList (); + // } + + // // + // // Claculate Required Info ... + // int totalCount = result.CountAsync ().Result; + // int maxId = nodes.Max (x => Convert.ToInt32 (GetKey (x))); + // int minId = nodes.Min (x => Convert.ToInt32 (GetKey (x))); + // bool hasNextPage = result.Any (x => Convert.ToInt32 (GetKey (x)) > maxId); + // bool hasPreviousPage = result.Any (x => Convert.ToInt32 (GetKey (x)) < minId); + + // // + // return Task.FromResult (new XPageResponse { + // Nodes = nodes, + // TotalCount = totalCount, + // HasNextPage = hasNextPage, + // HasPreviousPage = hasPreviousPage + // }); + // } + #endregion + + // + #region Update ... + public async Task UpdateAsync ( + TKey id, + T item, + bool saveChanges = true + ) { + // + // Get Exists Entity ... + var existsEntity = await GetAsync ( + id, + containsDetail : true, + ignoreSoftDeleteds : false + ); + + // + // Update Data ... + existsEntity.UpdateData (item); + var entry = dbSet.Attach (existsEntity); + entry.State = EntityState.Modified; + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .UpdateEvent (new XBaseEventModel (entry.Entity)); + } + + // + // Return result base on action Succeed ... + return isSucceed ? + entry.Entity : + null; + } + + public async Task UpdateRangeAsync ( + IEnumerable items, + bool saveChanges = true + ) { + // + items + .ToList () + .ForEach (i => { + var entry = dbSet.Attach (i); + entry.State = EntityState.Modified; + }); + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .UpdateManyEvent (new XBaseEventModel> (null)); + } + + // + // Return result base on action Succeed ... + return isSucceed; + } + #endregion + + // + #region Count ... + public async Task CountAsync (bool ignoreSoftDeleteds = true) { + // + var dbSet = GetDbSet (); + + // + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + return await dbSet.CountAsync (i => i.Deleted == false); + } else { + return await dbSet.CountAsync (); + } + } + + public async Task PagesCountAsync ( + int pageSize, + int? totalItems = null + ) { + // + int count = totalItems.HasValue ? totalItems.Value : await CountAsync (); + int pagesCount = count / pageSize; + + // + if (count % pageSize > 0) { + pagesCount++; + } + + // + return pagesCount; + } + #endregion + + // + #region Exists ... + public async Task IsExistsAsync ( + TKey id, + bool ignoreSoftDeleteds = true + ) { + // + var result = await FindOneAsync (i => + GetKey (i) + .ToString () == id + .ToString (), + ignoreSoftDeleteds : ignoreSoftDeleteds, + containsDetail : false + ); + + // + Detach (result); + + // + return !result.IsNull (); + } + #endregion + + // + #region Unit Of Work ... + public async Task SaveChangesAsync () { + return await unitOfWorks.SaveChangesAsync (); + } + #endregion + + // + #region Keys ... + public void SetKey ( + ref T item, + TKey id + ) { + // + var props = item.GetType ().GetProperties (); + var keyProp = props.FirstOrDefault (p => p.Name == "Id"); + if (keyProp.IsNull ()) { + return; + } + + // + Type t = Nullable.GetUnderlyingType (keyProp.PropertyType) ?? keyProp.PropertyType; + object safeValue = (id == null) ? null : Convert.ChangeType (id, t); + keyProp.SetValue (item, safeValue, null); + } + + public TKey GetKey (T item) { + // + var props = item.GetType ().GetProperties (); + var keyProp = props.FirstOrDefault (p => p.Name == "Id"); + + // + var keyString = string.Empty; + if (keyProp.IsNull ()) { + keyString = string.Empty; + } else { + keyString = keyProp.GetValue (item).ToString (); + } + + // + if (keyString.IsNullOrEmpty ()) { + return default (TKey); + } + + // + // Prevent Deserializing issues throug JsonReader ... + if (keyString.IsGuid () && typeof (TKey) == typeof (Guid)) { + return item.Id; + } + + // + return keyString.FromJSON (); + } + + public async Task HandleKeyAsync (T item) { + // + var keyType = typeof (TKey); + + // + // Handle Guid Key Type ... + // Since EFCore has AutoIncrement on int Ids, there is no need to handle int Key types ... + if ( + ( + keyType == typeof (Guid) || + keyType == typeof (string) + ) && + keyGenerator.IsEmpty (item.Id) + ) { + // + var newKey = await keyGenerator.GenerateKey (this); + + // + SetKey (ref item, newKey); + } + + // + return item; + } + #endregion + + // + #region Detach ... + public void Detach (T item) { + // + if (item.IsNull ()) { + return; + } + + // + Entry (item).State = EntityState.Detached; + } + + public void Detach (IEnumerable items) { + // + if (items.IsNull () || !items.HasChild ()) { + return; + } + + // + items + .ToList () + .ForEach (item => { + Detach (item); + }); + } + + public void Detach (XQueryResult query) { + // + if (query.IsNull () || query.Items.HasChild ()) { + return; + } + + // + Detach (query.Items); + } + + public void Detach (XPageResponse page) { + // + if (page.IsNull () || !page.Nodes.HasChild ()) { + return; + } + + // + Detach (page.Nodes); + } + #endregion + + // + #region Others ... + public void Dispose () { + unitOfWorks.Dispose (); + } + + public string GetPropValues (T item) { + // + var props = item.GetType ().GetProperties (); + var vals = props.Select (p => p.GetValue (p.Name)); + + // + return vals.ToJSON (); + } + + public EntityEntry Entry (T item) { + return unitOfWorks.DbContext.Entry (item); + } + + public IQueryable GetDbSet ( + bool containsDetail = false + ) { + // + IQueryable result = null; + if (!containsDetail) { + result = dbSet; + } else { + result = GetFullDbSet (); + } + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Events/XBaseRepositoryEvents.cs b/Events/XBaseRepositoryEvents.cs new file mode 100644 index 0000000..2298d62 --- /dev/null +++ b/Events/XBaseRepositoryEvents.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xDataService.Events { + /// + /// Some Events can be Raised on Some Actions Happens on Repositories ... + /// this is the Base Events Class ... + /// + /// + public abstract class XBaseRepositoryEvents : IXBaseRepositoryEvents { + // + private readonly ISubject> onAddSubject; + private readonly ISubject> onUpdateSubject; + private readonly ISubject> onRemoveSubject; + + // + private readonly ISubject>> onAddManySubject; + private readonly ISubject>> onUpdateManySubject; + private readonly ISubject>> onRemoveManySubject; + + // + protected XBaseRepositoryEvents () { + // + onAddSubject = new ReplaySubject> (1); + onUpdateSubject = new ReplaySubject> (1); + onRemoveSubject = new ReplaySubject> (1); + + // + onAddManySubject = new ReplaySubject>> (1); + onUpdateManySubject = new ReplaySubject>> (1); + onRemoveManySubject = new ReplaySubject>> (1); + } + + // + public void AddEvent (XBaseEventModel model) => onAddSubject.OnNext (model); + + public void UpdateEvent (XBaseEventModel model) => onUpdateSubject.OnNext (model); + + public void RemoveEvent (XBaseEventModel model) => onRemoveSubject.OnNext (model); + + // + public void AddManyEvent (XBaseEventModel> model) => onAddManySubject.OnNext (model); + public void UpdateManyEvent (XBaseEventModel> model) => onUpdateManySubject.OnNext (model); + public void RemoveManyEvent (XBaseEventModel> model) => onRemoveManySubject.OnNext (model); + + // + public IObservable> OnAddObservable => onAddSubject.AsObservable (); + + public IObservable> OnUpdateObservable => onUpdateSubject.AsObservable (); + + public IObservable> OnRemoveObservable => onRemoveSubject.AsObservable (); + + // + public IObservable>> OnAddManyObservable => onAddManySubject.AsObservable (); + public IObservable>> OnUpdateManyObservable => onUpdateManySubject.AsObservable (); + public IObservable>> OnRemoveManyObservable => onRemoveManySubject.AsObservable (); + + } +} \ No newline at end of file diff --git a/Extensions/DbContextOptionExtensions.cs b/Extensions/DbContextOptionExtensions.cs new file mode 100644 index 0000000..acde7ce --- /dev/null +++ b/Extensions/DbContextOptionExtensions.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using MySql.EntityFrameworkCore.Infrastructure; + +namespace xDataService.Extensions { + public static class DbContextOptionExtensions { + /// + /// convert dynamic object to MySqlDbContextOptionBuilder ... + /// Only Used in EFCore ... + /// + /// + /// + public static Action ToMySQLDbContextOptionsBuilder ( + this Action source + ) { + return ((Action) source); + } + + /// + /// convert dynamic object to SqliteDbContextOptionBuilder ... + /// Only Used in EFCore ... + /// + /// + /// + public static Action ToSqliteDbContextOptionsBuilder ( + this Action source + ) { + return ((Action) source); + } + + /// + /// convert dynamic object to SqlServerDbContextOptionBuilder ... + /// Only Used in EFCore ... + /// + /// + /// + public static Action ToSqlServerDbContextOptionsBuilder ( + this Action source + ) { + return ((Action) source); + } + } +} \ No newline at end of file diff --git a/Extensions/EntityExtensions.cs b/Extensions/EntityExtensions.cs new file mode 100644 index 0000000..d566044 --- /dev/null +++ b/Extensions/EntityExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using xCommons.Extensions; +using xModels.Base; + +namespace xDataService.Extensions { + public static class EntityExtensions { + /// + /// return all values of an entity properties as a json string ... + /// + /// + /// + /// + public static string GetPropValues (this T item) + where T : XBaseEntity { + // + var props = item.GetType ().GetProperties (); + var vals = props.Select (p => p.GetValue (item)); + + // + return vals.ToJSON (); + } + + /// + /// check values of all properties of an Entity contains specific value or not ... + /// + /// + /// + /// + /// + public static bool PropValuesContains (this T item, string value) + where T : XBaseEntity => item.GetPropValues () + .ToNormalString () + .Contains (value); + + /// + /// Retrieve Default Column Map of specific Entity ... + /// + /// + /// + /// + public static IDictionary>> GetDefaultColumnsMap (this T item) + where T : XBaseEntity { + // + if (item.IsNull ()) { + return null; + } + + // + var result = new Dictionary>> (); + var props = item.GetType ().GetProperties (); + + // + foreach (var prop in props) { + result.Add ( + prop.Name, + t => t.GetType ().GetProperty (prop.Name).GetValue (t, null)); + } + + // + return result; + } + + } +} \ No newline at end of file diff --git a/Extensions/IQueryableExtensions.cs b/Extensions/IQueryableExtensions.cs new file mode 100644 index 0000000..b03fc34 --- /dev/null +++ b/Extensions/IQueryableExtensions.cs @@ -0,0 +1,503 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using xCommons.Extensions; +using xDataService.Constants; +using xExceptions.Constants; +using xModels.Base; + +namespace xDataService.Extensions { + public static class IQueryableExtensions { + /// + /// Apply Search Filter on a Query + /// + /// + /// + /// + /// + public static IQueryable ApplyFilter ( + this IQueryable source, + string filter + ) + where T : XBaseEntity { + // + // Apply Filter ... + return source.Where (r => r.PropValuesContains (filter)); + } + + /// + /// Apply Search Filter on a Query + /// + /// + /// + /// + /// + public static async Task> ApplyFilterAsync ( + this IQueryable source, + string filter + ) + where T : XBaseEntity { + // + // Where Clause ... + Expression> whereClause = r => r + .PropValuesContains (filter); + + // + // Generate Where Function ... + var whereFunc = whereClause.Compile (); + + // + // Get Enumerable ... + var enumerator = source + .AsAsyncEnumerable (); + + // + var result = new List (); + await + foreach (var entity in enumerator) { + // + var isApproved = whereFunc (entity); + if (isApproved) { + result.Add (entity); + } + } + + // + return result.AsQueryable (); + } + + /// + /// Apply Search Filter on a Enumerable + /// + /// + /// + /// + /// + public static async Task> ApplyFilterAsync ( + this IEnumerable source, + string filter + ) + where T : XBaseEntity { + // + // Where Clause ... + Expression> whereClause = r => r + .PropValuesContains (filter); + + // + // Generate Where Function ... + var whereFunc = whereClause.Compile (); + + // + // Get Enumerable ... + var enumerator = source + .AsQueryable () + .AsAsyncEnumerable (); + + // + var result = new List (); + await + foreach (var entity in enumerator) { + // + var isApproved = whereFunc (entity); + if (isApproved) { + result.Add (entity); + } + } + + // + return result + .AsEnumerable (); + } + + /// + /// Apply Search Filter on a Enumerable + /// + /// + /// + /// + /// + public static IEnumerable ApplyFilter ( + this IEnumerable source, + string filter + ) + where T : class { + // + // Apply Filter ... + return source.Where (r => r.PropValuesContains (filter)); + } + + /// + /// provide a way to Order an IQueryable by string FieldName ... + /// + /// + /// + /// + /// + public static IOrderedQueryable OrderBy ( + this IQueryable query, + string propertyName + ) { + // + var entityType = typeof (TSource); + + // + // Create x => x.PropName + var propertyInfo = entityType.GetProperty (propertyName); + if (propertyInfo.IsNull ()) { + XException.InvalidArgs.Throw (); + } + + // + ParameterExpression arg = Expression.Parameter (entityType, "x"); + MemberExpression property = Expression.Property (arg, propertyName); + var selector = Expression.Lambda (property, new ParameterExpression[] { arg }); + + // + // Get System.Linq.Queryable.OrderBy() method. + var enumarableType = typeof (System.Linq.Queryable); + var method = enumarableType.GetMethods () + .Where (m => m.Name == "OrderBy" && m.IsGenericMethodDefinition) + .Where (m => { + // + var parameters = m.GetParameters ().ToList (); + + // + // Put more restriction here to ensure selecting the right overload + return parameters.Count == 2; // overload that has 2 parameters + }).Single (); + + // + // The linq's OrderBy has two generic types, which provided here ... + MethodInfo genericMethod = method + .MakeGenericMethod (entityType, propertyInfo.PropertyType); + + /* + Call query.OrderBy(selector), with query and selector: x=> x.PropName + Note that we pass the selector as Expression to the method and we don't compile it. + + By doing so EF can extract "order by" columns and generate SQL for it. + */ + var newQuery = (IOrderedQueryable) genericMethod + .Invoke (genericMethod, new object[] { query, selector }); + + // + return newQuery; + } + + /// + /// provide a way to Order an IEnumerable by string FieldName ... + /// + /// + /// + /// + /// + public static IOrderedEnumerable OrderBy ( + this IEnumerable query, + string propertyName + ) { + // + var entityType = typeof (TSource); + var propertyInfo = entityType.GetProperty (propertyName); + if (propertyInfo.IsNull ()) { + XException.InvalidArgs.Throw (); + } + + // + return query.OrderBy (x => propertyInfo.GetValue (x, null)); + } + + /// + /// provide a way to Order an IQueryable by string FieldName ... + /// + /// + /// + /// + /// + public static IOrderedQueryable OrderByDescending ( + this IQueryable query, + string propertyName + ) { + // + var entityType = typeof (TSource); + + // + // Create x => x.PropName + var propertyInfo = entityType.GetProperty (propertyName); + if (propertyInfo.IsNull ()) { + XException.InvalidArgs.Throw (); + } + + // + ParameterExpression arg = Expression.Parameter (entityType, "x"); + MemberExpression property = Expression.Property (arg, propertyName); + var selector = Expression.Lambda (property, new ParameterExpression[] { arg }); + + // + // Get System.Linq.Queryable.OrderBy() method. + var enumarableType = typeof (System.Linq.Queryable); + var method = enumarableType.GetMethods () + .Where (m => m.Name == "OrderByDescending" && m.IsGenericMethodDefinition) + .Where (m => { + // + var parameters = m.GetParameters ().ToList (); + + // + // Put more restriction here to ensure selecting the right overload + return parameters.Count == 2; // overload that has 2 parameters + }).Single (); + + // + // The linq's OrderBy has two generic types, which provided here ... + MethodInfo genericMethod = method + .MakeGenericMethod (entityType, propertyInfo.PropertyType); + + /* + Call query.OrderBy(selector), with query and selector: x=> x.PropName + Note that we pass the selector as Expression to the method and we don't compile it. + + By doing so EF can extract "order by" columns and generate SQL for it. + */ + var newQuery = (IOrderedQueryable) genericMethod + .Invoke (genericMethod, new object[] { query, selector }); + + // + return newQuery; + } + + /// + /// provide a way to Order an IEnumerable by string FieldName ... + /// + /// + /// + /// + /// + public static IOrderedEnumerable OrderByDescending ( + this IEnumerable query, + string propertyName + ) { + // + var entityType = typeof (TSource); + var propertyInfo = entityType.GetProperty (propertyName); + if (propertyInfo.IsNull ()) { + XException.InvalidArgs.Throw (); + } + + // + return query.OrderByDescending (x => propertyInfo.GetValue (x, null)); + } + + /// + /// Apply Sorting on a query + /// + /// + /// + /// + /// + /// + /// + public static IQueryable ApplySorting ( + this IQueryable source, + string sortBy, + bool isAscending, + IDictionary>> columnsMap = null + ) + where T : XBaseEntity { + // + if (columnsMap == null) { + // + columnsMap = source.FirstOrDefault () + .GetDefaultColumnsMap (); + + // + if (columnsMap == null) { + return source; + } + } + + // + // Validate SortBy ... + var sortByItem = columnsMap.Keys + .FirstOrDefault (p => + p.ToNormalString () == sortBy.ToNormalString ()); + if (sortByItem.IsNullOrEmpty ()) { + return source; + } + + // + // Apply Sorting ... + if (isAscending) { + return source.OrderBy (sortByItem); + } else { + return source.OrderByDescending (sortByItem); + } + } + + /// + /// Apply Sorting on a query + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable ApplySorting ( + this IEnumerable source, + string sortBy, + bool isAscending, + IDictionary>> columnsMap = null + ) + where T : XBaseEntity { + // + if (columnsMap == null) { + // + columnsMap = source.FirstOrDefault () + .GetDefaultColumnsMap (); + + // + if (columnsMap == null) { + return source; + } + } + + // + // Validate SortBy ... + var sortByItem = columnsMap.Keys + .FirstOrDefault (p => + p.ToNormalString () == sortBy.ToNormalString ()); + if (sortByItem.IsNullOrEmpty ()) { + return source; + } + + // + // Apply Sorting ... + if (isAscending) { + return source.OrderBy (sortByItem); + } else { + return source.OrderByDescending (sortByItem); + } + } + + /// + /// Apply Sorting on a Enumerable + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable ApplySorting ( + this IEnumerable source, + string sortBy, + bool isAscending, + IDictionary>> columnsMap = null + ) + where T : class { + // + if (columnsMap == null) { + // + columnsMap = source.FirstOrDefault () + .GetDefaultColumnsMap (); + + // + if (columnsMap == null) { + return source; + } + } + + // + // Validate SortBy ... + var sortByItem = columnsMap.Keys + .FirstOrDefault (p => + p.ToNormalString () == sortBy.ToNormalString ()); + if (sortByItem.IsNullOrEmpty ()) { + return source; + } + + // + // Apply Sorting ... + if (isAscending) { + return source.OrderBy (sortByItem); + } else { + return source.OrderByDescending (sortByItem); + } + } + + /// + /// Apply Paging on a qury + /// + /// + /// + /// + /// + /// + public static IQueryable ApplyPaging ( + this IQueryable source, + int? page, + int? pageSize + ) { + // + if (!page.HasValue || + page <= 0) { + page = 1; + } + + // + if (!pageSize.HasValue || + pageSize <= 0 || + pageSize > XDataServiceConstants.MAX_AVAILABLE_PAGE_SIZE) { + pageSize = XDataServiceConstants.DEFAULT_PAGE_SIZE; + } + + // + var pageVal = page.GetValueOrDefault (); + var pageSizeVal = pageSize.GetValueOrDefault (); + + // + return source + .Skip ((pageVal - 1) * pageSizeVal) + .Take (pageSizeVal); + } + + /// + /// Apply Paging on a Enumerable + /// + /// + /// + /// + /// + /// + public static IEnumerable ApplyPaging ( + this IEnumerable source, + int? page, + int? pageSize + ) { + // + if (!page.HasValue || + page <= 0) { + page = 1; + } + + // + if (!pageSize.HasValue || + pageSize <= 0 || + pageSize > XDataServiceConstants.MAX_AVAILABLE_PAGE_SIZE) { + pageSize = XDataServiceConstants.DEFAULT_PAGE_SIZE; + } + + // + var pageVal = page.GetValueOrDefault (); + var pageSizeVal = pageSize.GetValueOrDefault (); + + // + return source + .Skip ((pageVal - 1) * pageSizeVal) + .Take (pageSizeVal); + } + } +} \ No newline at end of file diff --git a/Extensions/XDataServiceConfigurationExtensions.cs b/Extensions/XDataServiceConfigurationExtensions.cs new file mode 100644 index 0000000..0371c6c --- /dev/null +++ b/Extensions/XDataServiceConfigurationExtensions.cs @@ -0,0 +1,82 @@ +using System.Linq; +using xCommons.Extensions; +using xDataService.Configuration; +using xDataService.Constants; + +namespace xDataService.Extensions { + public static class XDataServiceConfigurationExtensions { + /// + /// Extract Provider enum from Provider Type ... + /// + /// + /// + public static XDbProviders ToDbProvider (this string source) { + // + if (source.IsNullOrEmpty ()) { + return XDbProviders.None; + } + + // + source = source.ToNormalString (); + + // + var result = source == ProviderType.MySQL.ToNormalString () ? + XDbProviders.MySQL : + source == ProviderType.SQLite.ToNormalString () ? + XDbProviders.SQLite : + source == ProviderType.SQLServer.ToNormalString () ? + XDbProviders.SQLServer : + source == ProviderType.MongoDB.ToNormalString () ? + XDbProviders.MongoDB : + XDbProviders.None; + + // + return result; + } + + /// + /// Retrieve Mongo URI from ConnectionString + /// + /// + /// + public static string GetMongoDbURI (this XDataServiceConfiguration source) { + // + var parts = source.GetConnectionParts (); + var result = parts + .Where (p => p.Trim ().StartsWith ("Uri=")) + .FirstOrDefault (); + result = result.Replace ("Uri=", "").Trim (); + + // + return result; + } + + /// + /// Retrieve Mongo Database Name from Connaction string + /// + /// + /// + public static string GetMongoDbDatabase (this XDataServiceConfiguration source) { + // + var parts = source.GetConnectionParts (); + var result = parts + .Where (p => p.Trim ().StartsWith ("Database=")) + .FirstOrDefault (); + result = result.Replace ("Database=", "").Trim (); + + // + return result; + } + + // + #region Private ... + private static string[] GetConnectionParts (this XDataServiceConfiguration source) { + // + var result = source.ConnectionString.Split (';'); + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Extensions/XGraphQLExtensions.cs b/Extensions/XGraphQLExtensions.cs new file mode 100644 index 0000000..8ca6de9 --- /dev/null +++ b/Extensions/XGraphQLExtensions.cs @@ -0,0 +1,28 @@ +using GraphQL; +using GraphQL.Types; +using xDataService.Constants; +using xModels.Dtos; + +namespace xDataService.Extensions { + public static class XGraphQLExtensions { + public static TKey GetIdArgument (this IResolveFieldContext source) { + return source.GetArgument (XGraphQLHelper.GetIdArgument().Name); + } + + public static bool GetIgnoreSoftDeletedArgument (this IResolveFieldContext source) { + return source.GetArgument (XGraphQLHelper.IgnoreSoftDeletedArgument.Name); + } + + public static bool GetContainsDetailArgument (this IResolveFieldContext source) { + return source.GetArgument (XGraphQLHelper.ContainsDetailArgument.Name); + } + + public static string GetSearchQueryArgument (this IResolveFieldContext source) { + return source.GetArgument (XGraphQLHelper.SearchQueryArgument.Name); + } + + public static XQuery GetQueryArgument (this IResolveFieldContext source) { + return source.GetArgument (XGraphQLHelper.QueryArgument.Name); + } + } +} \ No newline at end of file diff --git a/Extensions/XMongoDbExtensions.cs b/Extensions/XMongoDbExtensions.cs new file mode 100644 index 0000000..319ab77 --- /dev/null +++ b/Extensions/XMongoDbExtensions.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Core.Bindings; +using MongoDB.Driver.Core.Clusters; +using MongoDB.Driver.Core.Servers; +using MongoDB.Driver.Core.WireProtocol.Messages.Encoders; +using MongoDB.Driver.Linq; +using xDataService.Mongo; + +namespace xDataService.Extensions { + public static class XMongoDbExtensions { + public static Guid ToGuid (this ObjectId source) { + // + var bytes = source + .ToByteArray () + .Concat (new byte[] { 5, 5, 5, 5 }) + .ToArray (); + + // + var result = new Guid (bytes); + + // + return result; + } + + public static ObjectId ToObjectId (this Guid source) { + // + var bytes = source + .ToByteArray () + .Take (12) + .ToArray (); + + // + var result = new ObjectId (bytes); + + // + return result; + } + + public static IAsyncEnumerable ToAsyncEnumerable (this IAsyncCursorSource asyncCursorSource) { + return new XAsyncEnumerableAdapter (asyncCursorSource); + } + + public static async IAsyncEnumerable ToAsyncEnumerable (this IAsyncCursor source) { + while (await source.MoveNextAsync ()) { + foreach (var current in source.Current) { + yield return current; + } + } + } + + public static XMongoAsyncCursor ToAsyncCursor (this IQueryable source) { + return new XMongoAsyncCursor (source); + } + } +} \ No newline at end of file diff --git a/GraphQL/XBaseGraphObjectType.cs b/GraphQL/XBaseGraphObjectType.cs new file mode 100644 index 0000000..96c47a5 --- /dev/null +++ b/GraphQL/XBaseGraphObjectType.cs @@ -0,0 +1,21 @@ +using GraphQL.Types; +using xModels.Base; + +namespace xDataService.GraphQL +{ + /// + /// Base Entity GraphType Object ... + /// + /// + public abstract class XBaseGraphObjectType : ObjectGraphType + where T : XBaseEntity { + public XBaseGraphObjectType () { + // + Name = $"{typeof(T).Name}GraphType"; + + // + Field (x => x.Id); + Field (x => x.Deleted); + } + } +} \ No newline at end of file diff --git a/GraphQL/XBaseGraphQLEventType.cs b/GraphQL/XBaseGraphQLEventType.cs new file mode 100644 index 0000000..9febbc4 --- /dev/null +++ b/GraphQL/XBaseGraphQLEventType.cs @@ -0,0 +1,17 @@ +using GraphQL.Types; +using xDataService.Models; +using xModels.Base; + +namespace xDataService.GraphQL { + public class XBaseGraphQLEventType : ObjectGraphType> + where TGraphType : IGraphType + where TEntity : XBaseEntity { + public XBaseGraphQLEventType () { + // + Name = nameof (XBaseGraphQLEventType); + + // + Field (x => x.Model, type : typeof (TGraphType)); + } + } +} \ No newline at end of file diff --git a/GraphQL/XBaseGraphQLQuery.cs b/GraphQL/XBaseGraphQLQuery.cs new file mode 100644 index 0000000..6c7ca14 --- /dev/null +++ b/GraphQL/XBaseGraphQLQuery.cs @@ -0,0 +1,167 @@ +using System; +using System.Linq.Expressions; +using GraphQL.Types; +using xCommons.Extensions; +using xDataService.Configuration; +using xDataService.Constants; +using xDataService.Extensions; +using xDataService.Interfaces; +using xDataService.Models; +using xModels.Base; + +namespace xDataService.GraphQL { + public abstract class XBaseGraphQLQuery : ObjectGraphType, IXBaseGraphQLQuery + where TEntity : XBaseEntity + where TGraph : IGraphType + where TGraphKey : IGraphType { + // + private readonly XDataServiceConfiguration configuration; + private readonly IXBaseRepository repository; + private readonly IXBaseGraphQLTypeHelper helper; + + // + public XBaseGraphQLQuery ( + XDataServiceConfiguration configuration, + IXBaseRepository repository, + IXBaseGraphQLTypeHelper helper + ) { + // + this.helper = helper; + this.repository = repository; + this.configuration = configuration; + + // + Name = GetType ().Name; + + // + #region Retrieve ... + // + // Get ... + FieldAsync ( + name: helper.GetInQuerySingleName (), + arguments: new QueryArguments ( + XGraphQLHelper.GetIdArgument (), + XGraphQLHelper.IgnoreSoftDeletedArgument, + XGraphQLHelper.ContainsDetailArgument + ), + resolve : async context => + await repository + .GetAsync ( + id: context.GetIdArgument (), + ignoreSoftDeleteds: context.GetIgnoreSoftDeletedArgument (), + containsDetail: context.GetContainsDetailArgument () + ) + .ToDynamicObject () + ); + + // + // GetAll ... + FieldAsync> ( + name: helper.GetInQueryCollectionName (), + arguments: new QueryArguments ( + XGraphQLHelper.IgnoreSoftDeletedArgument, + XGraphQLHelper.ContainsDetailArgument + ), + resolve : async context => + await repository + .GetAllAsync ( + ignoreSoftDeleteds: context.GetIgnoreSoftDeletedArgument (), + containsDetail: context.GetContainsDetailArgument () + ) + .ToDynamicObject () + ); + + // + // FindOne ... + FieldAsync ( + name: helper.GetFindOneName (), + arguments: new QueryArguments ( + XGraphQLHelper.SearchQueryArgument, + XGraphQLHelper.IgnoreSoftDeletedArgument, + XGraphQLHelper.ContainsDetailArgument + ), + resolve : async (context) => { + // + var searchQuery = context.GetSearchQueryArgument (); + Expression> whereClase = pe => pe + .PropValuesContains (searchQuery); + + // + return await repository + .FindOneAsync ( + whereClause: whereClase, + ignoreSoftDeleteds: context.GetIgnoreSoftDeletedArgument (), + containsDetail: context.GetContainsDetailArgument () + ) + .ToDynamicObject (); + } + ); + + // + // FindMany ... + FieldAsync> ( + name: helper.GetFindManyName (), + arguments: new QueryArguments ( + XGraphQLHelper.SearchQueryArgument, + XGraphQLHelper.IgnoreSoftDeletedArgument, + XGraphQLHelper.ContainsDetailArgument + ), + resolve : async (context) => { + // + var searchQuery = context.GetSearchQueryArgument (); + Expression> whereClase = pe => pe + .PropValuesContains (searchQuery); + + // + return await repository + .FindManyAsync ( + whereClause: whereClase, + ignoreSoftDeleteds: context.GetIgnoreSoftDeletedArgument (), + containsDetail: context.GetContainsDetailArgument () + ) + .ToDynamicObject (); + } + ); + + // + // Query ... + FieldAsync> ( + name: helper.GetQueryName (), + arguments: new QueryArguments ( + XGraphQLHelper.QueryArgument, + XGraphQLHelper.IgnoreSoftDeletedArgument + ), + resolve : async context => + await repository + .QueryAsync ( + query: context.GetQueryArgument (), + ignoreSoftDeleteds: context.GetIgnoreSoftDeletedArgument () + ) + .ToDynamicObject () + ); + + // + // Count ... + FieldAsync ( + name: helper.GetCountName (), + resolve: async context => + await repository + .CountAsync () + ); + + // + // Exists ... + FieldAsync ( + name: helper.GetExistsName (), + arguments: new QueryArguments ( + XGraphQLHelper.GetIdArgument () + ), + resolve : async context => + await repository.IsExistsAsync ( + context.GetIdArgument () + ) + ); + #endregion + } + } +} \ No newline at end of file diff --git a/GraphQL/XBaseGraphQLSubscription.cs b/GraphQL/XBaseGraphQLSubscription.cs new file mode 100644 index 0000000..9005c93 --- /dev/null +++ b/GraphQL/XBaseGraphQLSubscription.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using GraphQL.Resolvers; +using GraphQL.Types; +using xCommons.Extensions; +using xDataService.Events; +using xDataService.Interfaces; +using xDataService.Models; +using xModels.Base; + +namespace xDataService.GraphQL { + public abstract class XBaseGraphQLSubscription : ObjectGraphType + where TGraph : IGraphType + where TEntity : XBaseEntity { + public XBaseGraphQLSubscription (IXBaseRepositoryEvents eventProvider = null) { + // + Name = GetType ().Name; + + // + // Check if base repository patter registered ... + if (!eventProvider.IsNull ()) { + // + // Add ... + AddField (new EventStreamFieldType { + // + Name = nameof (XBaseRepositoryEvents.AddEvent), + Type = typeof (XBaseGraphQLEventType), + Resolver = new FuncFieldResolver> ( + context => context.Source as XBaseEventModel + ), + Subscriber = new EventStreamResolver> (context => { + return eventProvider.OnAddObservable; + }) + }); + + // + // Add Many ... + AddField (new EventStreamFieldType { + // + Name = nameof (XBaseRepositoryEvents.AddManyEvent), + Type = typeof (XBaseGraphQLEventType), + Resolver = new FuncFieldResolver>> ( + context => context.Source as XBaseEventModel> + ), + Subscriber = new EventStreamResolver>> (context => { + return eventProvider.OnAddManyObservable; + }) + }); + + // + // Update ... + AddField (new EventStreamFieldType { + // + Name = nameof (XBaseRepositoryEvents.UpdateEvent), + Type = typeof (XBaseGraphQLEventType), + Resolver = new FuncFieldResolver> ( + context => context.Source as XBaseEventModel + ), + Subscriber = new EventStreamResolver> (context => { + return eventProvider.OnUpdateObservable; + }) + }); + + // + // Update Mnay ... + AddField (new EventStreamFieldType { + // + Name = nameof (XBaseRepositoryEvents.UpdateManyEvent), + Type = typeof (XBaseGraphQLEventType), + Resolver = new FuncFieldResolver>> ( + context => context.Source as XBaseEventModel> + ), + Subscriber = new EventStreamResolver>> (context => { + return eventProvider.OnUpdateManyObservable; + }) + }); + + // + // Remove ... + AddField (new EventStreamFieldType { + // + Name = nameof (XBaseRepositoryEvents.RemoveEvent), + Type = typeof (XBaseGraphQLEventType), + Resolver = new FuncFieldResolver> ( + context => context.Source as XBaseEventModel + ), + Subscriber = new EventStreamResolver> (context => { + return eventProvider.OnRemoveObservable; + }) + }); + + // + // Remove Many ... + AddField (new EventStreamFieldType { + // + Name = nameof (XBaseRepositoryEvents.RemoveManyEvent), + Type = typeof (XBaseGraphQLEventType), + Resolver = new FuncFieldResolver>> ( + context => context.Source as XBaseEventModel> + ), + Subscriber = new EventStreamResolver>> (context => { + return eventProvider.OnRemoveManyObservable; + }) + }); + } + } + } +} \ No newline at end of file diff --git a/GraphQL/XBaseGraphQLTypeHelper.cs b/GraphQL/XBaseGraphQLTypeHelper.cs new file mode 100644 index 0000000..3d7986c --- /dev/null +++ b/GraphQL/XBaseGraphQLTypeHelper.cs @@ -0,0 +1,64 @@ +using xCommons.Extensions; +using xDataService.Configuration; +using xDataService.Interfaces; +using xModels.Base; + +namespace xDataService.GraphQL { + public abstract class XBaseGraphQLTypeHelper : IXBaseGraphQLTypeHelper + where TEntity : XBaseEntity { + // + private readonly XDataServiceConfiguration configuration; + + // + public XBaseGraphQLTypeHelper ( + XDataServiceConfiguration configuration = null + ) { + this.configuration = configuration; + } + + // + public abstract string GetInQueryCollectionName (); + + public abstract string GetInQuerySingleName (); + + // + public string GetFindOneName () { + return $"find{GetInQuerySingleName ().ToNormalString().Capitalize()}"; + } + + public string GetFindManyName () { + return $"find{GetInQueryCollectionName ().ToNormalString().Capitalize()}"; + } + + public string GetQueryName () { + return $"query{GetInQueryCollectionName().ToNormalString().Capitalize()}"; + } + + public string GetCountName () { + return $"count{GetInQueryCollectionName().ToNormalString().Capitalize()}"; + } + + public string GetExistsName () { + return $"exists{GetInQuerySingleName().ToNormalString().Capitalize()}"; + } + + public string GetGraphQLPath (string baseGraphPath = null) { + // + baseGraphPath = baseGraphPath.IsNullOrEmpty () ? + configuration.IsNull () || configuration.GraphQLBasePath.IsNullOrEmpty () ? + "" : + configuration.GraphQLBasePath : + baseGraphPath; + + // + var basePath = baseGraphPath + .EndsWith ("/") ? + baseGraphPath + .Substring (0, baseGraphPath.Length - 1) : + baseGraphPath; + + // + return $"{basePath}/{GetInQueryCollectionName().ToNormalString()}"; + } + } +} \ No newline at end of file diff --git a/Helpers/XCursorHelper.cs b/Helpers/XCursorHelper.cs new file mode 100644 index 0000000..a113c64 --- /dev/null +++ b/Helpers/XCursorHelper.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using xCommons.Extensions; + +namespace xDataService.Helpers { + public partial class XCursorHelper { + public static string ToCursor (T id) { + // + // Check Numbers ... + if (id.ToString ().IsDigits ()) { + return Convert + .ToBase64String (BitConverter + .GetBytes (Convert.ToInt32 (id)) + ); + } else + // + // Check Guid ... + if (id.ToString ().IsGuid ()) { + return Convert + .ToBase64String (new Guid (id.ToString ()) + .ToByteArray ()); + } else + // + // Assume as String ... + { + return Convert + .ToBase64String (id + .ToString ().ToBytes ()); + } + } + + public static T FromCursor (string cursor) { + // + // Check Numbers ... + var returnType = typeof (T); + if (returnType == typeof (int) || + returnType == typeof (long) || + returnType == typeof (decimal)) { + return Convert.ChangeType ((BitConverter + .ToInt32 (Convert + .FromBase64String (cursor), + 0 + )) + .ToDynamicObject (), returnType); + } else + // + // Check Guid ... + if (returnType == typeof (Guid)) { + return Convert.ChangeType ((new Guid ( + Convert.FromBase64String (cursor) + )) + .ToDynamicObject (), returnType); + } else + // + // Assume as String ... + { + return Convert.ChangeType (( + Convert.FromBase64String (cursor) + ) + .ToDynamicObject (), returnType); + } + } + + public static (string firstCursor, string lastCursor) GetFirstAndLastCursor (IEnumerable ids) { + // + if (ids?.Any () != true) { + return (null, null); + } + + // + var firstCursor = ToCursor (ids.First ()); + var lastCursor = ToCursor (ids.Last ()); + + // + return (firstCursor, lastCursor); + } + } +} \ No newline at end of file diff --git a/Helpers/XGraphQLHelper.cs b/Helpers/XGraphQLHelper.cs new file mode 100644 index 0000000..7d7b2d9 --- /dev/null +++ b/Helpers/XGraphQLHelper.cs @@ -0,0 +1,38 @@ +using GraphQL.Types; +using xDataService.Models; +using xModels.Dtos; + +namespace xDataService.Constants { + public static class XGraphQLHelper { + public static QueryArgument GetIdArgument () + where TKey : IGraphType { + return new QueryArgument { + Name = "id", + Description = "the id of entity, which you want to retrieve ..." + }; + } + + public static QueryArgument IgnoreSoftDeletedArgument = new QueryArgument { + Name = "ignoreSoftDeleteds", + DefaultValue = true, + Description = "determines result should ignore soft deleted items or not ..." + }; + + public static QueryArgument ContainsDetailArgument = new QueryArgument { + Name = "containsDetail", + DefaultValue = false, + Description = "determines result should contains navigation properties or not ..." + }; + + public static QueryArgument SearchQueryArgument = new QueryArgument { + Name = "searchQuery", + Description = "the value which had to looking for ..." + }; + + public static QueryArgument QueryArgument = new QueryArgument { + Name = "query", + DefaultValue = new XQuery (), + Description = "determines query structure info for retrieving data ..." + }; + } +} \ No newline at end of file diff --git a/Interfaces/IXBaseGraphQLQuery.cs b/Interfaces/IXBaseGraphQLQuery.cs new file mode 100644 index 0000000..8c84cef --- /dev/null +++ b/Interfaces/IXBaseGraphQLQuery.cs @@ -0,0 +1,9 @@ +using GraphQL.Types; +using xModels.Base; + +namespace xDataService.Interfaces { + public interface IXBaseGraphQLQuery + where TEntity : XBaseEntity + where TGraph : IGraphType + where TGraphKey : IGraphType { } +} \ No newline at end of file diff --git a/Interfaces/IXBaseGraphQLTypeHelper.cs b/Interfaces/IXBaseGraphQLTypeHelper.cs new file mode 100644 index 0000000..de2548e --- /dev/null +++ b/Interfaces/IXBaseGraphQLTypeHelper.cs @@ -0,0 +1,21 @@ +using xModels.Base; + +namespace xDataService.Interfaces { + public interface IXBaseGraphQLTypeHelper + where TEntity : XBaseEntity { + string GetInQuerySingleName (); + string GetInQueryCollectionName (); + + string GetFindOneName (); + + string GetFindManyName (); + + string GetQueryName (); + + string GetCountName (); + + string GetExistsName (); + + string GetGraphQLPath (string baseGraphPath = null); + } +} \ No newline at end of file diff --git a/Interfaces/IXBaseRepository.cs b/Interfaces/IXBaseRepository.cs new file mode 100644 index 0000000..9585c33 --- /dev/null +++ b/Interfaces/IXBaseRepository.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using xModels.Base; +using xModels.Dtos; + +namespace xDataService.Interfaces { + /// + /// a Base Repository Interface for Manipulate + /// an Entity in DataBase + /// + /// + public interface IXBaseRepository : IDisposable + where T : XBaseEntity { + // + #region Add ... + /// + /// add a new Entity ... + /// + /// + /// + /// + Task AddAsync ( + T item, + bool saveChanges = true + ); + + /// + /// add or update an Entity (add if not exists/update if exists) ... + /// + /// + /// + /// + Task AddOrUpdateAsync ( + T item, + bool saveChanges = true + ); + + /// + /// add a range of new Entities ... + /// + /// + /// + /// + Task AddRangeAsync ( + IEnumerable items, + bool saveChanges = true + ); + #endregion + + // + #region Remove ... + /// + /// remove an Entity by it's Id ... + /// + /// + /// + /// + /// + Task RemoveAsync ( + TKey id, + bool softDelete = true, + bool saveChanges = true + ); + + /// + /// remove an Entity ... + /// + /// + /// + /// + /// + Task RemoveAsync ( + T item, + bool softDelete = true, + bool saveChanges = true + ); + + /// + /// remove a range of exists Entities ... + /// + /// + /// + /// + /// + Task RemoveRangeAsync ( + IEnumerable items, + bool softDelete = true, + bool saveChanges = true + ); + #endregion + + // + #region Retrieve ... + /// + /// retrieve whole items as queryable ... + /// + /// + IQueryable AsQueryable (); + + /// + /// retrieve an Entity by it's Id ... + /// + /// + /// + /// + /// + Task GetAsync ( + TKey id, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ); + + /// + /// retrieve all exists Entities ... + /// + /// + /// + /// + Task> GetAllAsync ( + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ); + + /// + /// find an Entity by providing a Conditional Expression ... + /// + /// + /// + /// + /// + Task FindOneAsync ( + Expression> whereClause, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ); + + /// + /// find a collection of Entities by proving a Conditional Expression ... + /// + /// + /// + /// + /// + Task> FindManyAsync ( + Expression> whereClause, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ); + + /// + /// retrieve Entities based on XQuery Pagination structure ... + /// + /// + /// + /// + Task> QueryAsync ( + XQuery query, + bool ignoreSoftDeleteds = true + ); + + /// + /// retrieve Entities based on XQuery Pagination structure by providing a Conditional Expression ... + /// + /// + /// + /// + /// + Task> ConditionalQueryAsync ( + Expression> whereClause, + XQuery query, + bool ignoreSoftDeleteds = true + ); + #endregion + + // + #region Update ... + /// + /// Update an Entity values ... + /// + /// + /// + /// + /// + Task UpdateAsync ( + TKey id, + T item, + bool saveChanges = true + ); + + /// + /// Update a range of Entities ... + /// + /// + /// + /// + Task UpdateRangeAsync ( + IEnumerable items, + bool saveChanges = true + ); + #endregion + + // + #region Count ... + /// + /// count all exists Entities ... + /// + /// + /// + Task CountAsync (bool ignoreSoftDeleteds = true); + + /// + /// count all exists Entities Pages by providing page size ... + /// + /// + /// + /// + Task PagesCountAsync ( + int pageSize, + int? totalItems = null + ); + #endregion + + // + #region Exists ... + /// + /// Check an Entity exists or not ... + /// + /// + /// + /// + Task IsExistsAsync ( + TKey id, + bool ignoreSoftDeleteds = true + ); + #endregion + + // + #region Unit Of Work ... + /// + /// Save all unsaved Transactions on DbContext ... + /// used fo Unit Of Works Design Pattern ... + /// + /// + Task SaveChangesAsync (); + #endregion + + // + #region Key ... + TKey GetKey (T item); + + void SetKey ( + ref T item, + TKey id + ); + + Task HandleKeyAsync (T item); + #endregion + + // + #region Detach ... + void Detach (T item); + + void Detach (IEnumerable items); + + void Detach (XQueryResult query); + + void Detach (XPageResponse page); + #endregion + } +} \ No newline at end of file diff --git a/Interfaces/IXBaseRepositoryEvents.cs b/Interfaces/IXBaseRepositoryEvents.cs new file mode 100644 index 0000000..699c1d5 --- /dev/null +++ b/Interfaces/IXBaseRepositoryEvents.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using xDataService.Models; + +namespace xDataService.Interfaces { + public interface IXBaseRepositoryEvents { + // + void AddEvent (XBaseEventModel model); + void UpdateEvent (XBaseEventModel model); + void RemoveEvent (XBaseEventModel model); + + // + void AddManyEvent (XBaseEventModel> model); + void UpdateManyEvent (XBaseEventModel> model); + void RemoveManyEvent (XBaseEventModel> model); + + // + IObservable> OnAddObservable { get; } + IObservable> OnUpdateObservable { get; } + IObservable> OnRemoveObservable { get; } + + // + IObservable>> OnAddManyObservable { get; } + IObservable>> OnUpdateManyObservable { get; } + IObservable>> OnRemoveManyObservable { get; } + } +} \ No newline at end of file diff --git a/Interfaces/IXDataServiceHelper.cs b/Interfaces/IXDataServiceHelper.cs new file mode 100644 index 0000000..7723d47 --- /dev/null +++ b/Interfaces/IXDataServiceHelper.cs @@ -0,0 +1,78 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson.Serialization.Conventions; +using xDataService.Constants; + +namespace xDataService.Interfaces { + public interface IXDataServiceHelper { + /// + /// register DbContext implementation on DI Container ... + /// Only Used in EFCore ... + /// + /// + /// + /// + /// + /// + void AddDbContext ( + IServiceCollection services, + string connectionString, + XDbProviders dbProvider, + ServiceLifetime lifetime, + Action optionsBuilder = null + ); + + /// + /// register all Entities Repositories and Events on DI Container ... + /// implementations of IXBaseRepository and IXBaseRepositoryEvent ... + /// + /// + /// + void AddRepositories ( + IServiceCollection services, + ServiceLifetime lifetime + ); + + /// + /// Regitster KeyGenerators for Repositories ... + /// + /// + void AddKeyGenerators (IServiceCollection services); + + /// + /// register DBSeeder Configuration on DI Container ... + /// + /// + /// + void AddDbSeederConfiguration ( + IServiceCollection services, + IConfiguration configuration + ); + + /// + /// register Data Seeder on DI Container ... + /// implementation of IXDbSeeder ... + /// + /// + /// + /// + /// + void AddDbSeeder ( + IServiceCollection services, + IConfiguration config, + ServiceLifetime lifetime + ) where TDbSeeder : IXDbSeeder; + + /// + /// Set Mongo Convention Pack ... + /// + /// + ConventionPack MongoConventionPacks (); + + /// + /// Configure all required MongoDb Class Mappers and etc here ... + /// + void RegisterMongoExtras (); + } +} \ No newline at end of file diff --git a/Interfaces/IXDbSeeder.cs b/Interfaces/IXDbSeeder.cs new file mode 100644 index 0000000..b0a2807 --- /dev/null +++ b/Interfaces/IXDbSeeder.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using xDataService.Configuration; + +namespace xDataService.Interfaces { + /// + /// Db Seeder used to seed data on Database on startup time ... + /// + public interface IXDbSeeder { + IXDbSeederConfig Config { get; } + XDataServiceConfiguration DataServiceConfiguration { get; } + + /// + /// do all seeding action by helping of Repositories here ... + /// don't forget to check UpdateExists value on IXDbSeederConfig for prevent system of duplicate Entity adding on startups ... + /// + /// + Task Seed (); + } +} \ No newline at end of file diff --git a/Interfaces/IXDbSeederConfig.cs b/Interfaces/IXDbSeederConfig.cs new file mode 100644 index 0000000..92d41ab --- /dev/null +++ b/Interfaces/IXDbSeederConfig.cs @@ -0,0 +1,14 @@ +namespace xDataService.Interfaces { + /// + /// this Configuration used to Carry all Entity Descriptors collections + /// can readable by appSettings.json file for providing dynamic data seeding ... + /// + public interface IXDbSeederConfig { + /// + /// determines an Entity can Update when Exists or not ... + /// this must handled by consumer on IXDbSeeder Seed method implementation ... + /// + /// + bool UpdateExists { get; set; } + } +} \ No newline at end of file diff --git a/Interfaces/IXGraphQLHelper.cs b/Interfaces/IXGraphQLHelper.cs new file mode 100644 index 0000000..0c96bdb --- /dev/null +++ b/Interfaces/IXGraphQLHelper.cs @@ -0,0 +1,75 @@ +using GraphQL.Server; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace xDataService.Interfaces { + /// + /// this is a helper class which provides all requirements to providing GraphQL + /// based data manipulating mechanism ... + /// + public interface IXGraphQLHelper { + /// + /// Register all GraphQL Enum Types ... + /// Enum Types use to Map string values to Enum values in propper way and vise versa in GraphQL ... + /// must extended from EnumerationGraphType ... + /// + void AddXGraphEnumTypes (IServiceCollection services); + + /// + /// Register all GraphQL Object Types ... + /// GraphQL works based on Types, so for representing each data you have to define a Type ... + /// must extended from ObjectGraphType or XBaseGraphObjectType + /// + void AddXGraphObjectTypes (IServiceCollection services); + + /// + /// Register all GraphQL Input Types ... + /// InputTypes represent recieving data structure in GraphQL for doing mutations ... + /// must extended from InputObjectGraphType ... + /// + void AddXGraphInputTypes (IServiceCollection services); + + /// + /// Register all GraphQL Queries ... + /// Queries in GraphQL used to provide data fetch and retrieve mechanism ... + /// extended from ObjectGraphType ... + /// + void AddXGraphQueries (IServiceCollection services); + + /// + /// Register all GraphQL Mutations ... + /// Mutations is an Action types in GraphQL which data changed through them, like as Add, Update, Delete ... + /// must extends from ObjectGraphType ... + /// + void AddXGraphMutations (IServiceCollection services); + + /// + /// Register all GraphQL Subscriptions ... + /// Subscriptions are listeners to specific events on data in GraphQL ... + /// must extended from ObjectGraphType and implements IXBaseRepositoryEvents ... + /// + void AddXGraphSubscriptions (IServiceCollection services); + + /// + /// Register all GraphQL Schemas ... + /// each available Entity in Database must described to GraphQL with all Graph based stuffs such as : + /// Queries, Types and etc, this must done through an Schema class. + /// must extended from Schema ... + /// + void AddXGraphSchemas (IServiceCollection services); + + /// + /// Add Schemas to GraphQL Builder ... + /// here you must add all registered Schemas to GraphQLBuilder ... + /// + /// + /// + IGraphQLBuilder AddXGraphTypes (IGraphQLBuilder builder); + + /// + /// Use all Registered Schemas by GraphQL and and GraphQLWebSockets ... + /// + /// + void UseXGraph (IApplicationBuilder app); + } +} \ No newline at end of file diff --git a/Interfaces/IXKeyGenerator.cs b/Interfaces/IXKeyGenerator.cs new file mode 100644 index 0000000..3b595c4 --- /dev/null +++ b/Interfaces/IXKeyGenerator.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using xModels.Base; + +namespace xDataService.Interfaces { + public interface IXKeyGenerator + where TEntity : XBaseEntity { + bool IsEmpty (TKey id); + Task GenerateKey ( + IXBaseRepository repository + ); + } +} \ No newline at end of file diff --git a/Interfaces/IXSequentialGuid.cs b/Interfaces/IXSequentialGuid.cs new file mode 100644 index 0000000..80aebbc --- /dev/null +++ b/Interfaces/IXSequentialGuid.cs @@ -0,0 +1,11 @@ +using System; + +namespace xDataService.Interfaces { + /// + /// this service provide Sequential GUID mechanism for Entity Ids ... + /// + public interface IXSequentialGuid { + Guid GetCurrentGuid (); + Guid Next (); + } +} \ No newline at end of file diff --git a/Interfaces/IXUnitOfWorks.cs b/Interfaces/IXUnitOfWorks.cs new file mode 100644 index 0000000..825d775 --- /dev/null +++ b/Interfaces/IXUnitOfWorks.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using xDataService.Db; +using xModels.Base; + +namespace xDataService.Interfaces { + /// + /// Provide Unit Of Works Design Pattern for Db Transactions ... + /// Only Used on EFCore ... + /// + /// + public interface IXUnitOfWorks : IDisposable + where TDbContext : XDbContext { + TDbContext DbContext { get; } + DbSet GetDbSet () where T : XBaseEntity; + int SaveChanges (); + Task SaveChangesAsync (); + } +} \ No newline at end of file diff --git a/Models/XBaseEventModel.cs b/Models/XBaseEventModel.cs new file mode 100644 index 0000000..d114ac6 --- /dev/null +++ b/Models/XBaseEventModel.cs @@ -0,0 +1,13 @@ +namespace xDataService.Models { + /// + /// Pass Data on Repository Events ... + /// + /// + public class XBaseEventModel { + public T Model { get; set; } + + public XBaseEventModel (T model) { + Model = model; + } + } +} \ No newline at end of file diff --git a/Models/XGraphQueryResult.cs b/Models/XGraphQueryResult.cs new file mode 100644 index 0000000..049ac09 --- /dev/null +++ b/Models/XGraphQueryResult.cs @@ -0,0 +1,20 @@ +using GraphQL.Types; +using xModels.Dtos; + +namespace xDataService.Models { + public class XGraphQueryResult : ObjectGraphType> + where TGraph : IGraphType { + public XGraphQueryResult () { + // + Name = $"{typeof(TGraph).Name}QueryResult"; + + // + Field> (nameof (XQueryResult.Items)); + Field (x => x.Page); + Field (x => x.PageSize); + Field (x => x.TotalPages); + Field (x => x.TotalFilteredItems); + Field (x => x.TotalItems); + } + } +} \ No newline at end of file diff --git a/Models/XGraphQueryType.cs b/Models/XGraphQueryType.cs new file mode 100644 index 0000000..37cebb0 --- /dev/null +++ b/Models/XGraphQueryType.cs @@ -0,0 +1,19 @@ +using GraphQL.Types; +using xModels.Dtos; + +namespace xDataService.Models { + public class XGraphQueryType : InputObjectGraphType { + public XGraphQueryType () { + // + Name = "query"; + + // + Field (x => x.Filter); + Field (x => x.ContainsDetail); + Field (x => x.SortBy); + Field (x => x.IsAscending); + Field (x => x.Page); + Field (x => x.PageSize); + } + } +} \ No newline at end of file diff --git a/Mongo/XAsyncEnumerableAdapter.cs b/Mongo/XAsyncEnumerableAdapter.cs new file mode 100644 index 0000000..16fa7ee --- /dev/null +++ b/Mongo/XAsyncEnumerableAdapter.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace xDataService.Mongo { + public class XAsyncEnumerableAdapter : IAsyncEnumerable { + // + private readonly IAsyncCursorSource asyncCursorSource; + + // + public XAsyncEnumerableAdapter (IAsyncCursorSource asyncCursorSource) { + this.asyncCursorSource = asyncCursorSource; + } + + // + public IAsyncEnumerator GetAsyncEnumerator (CancellationToken cancellationToken) => + new XAsyncEnumeratorAdapter (asyncCursorSource, cancellationToken); + } + + public class XAsyncEnumeratorAdapter : IAsyncEnumerator { + private readonly IAsyncCursorSource asyncCursorSource; + private readonly CancellationToken cancellationToken; + private IAsyncCursor asyncCursor; + private IEnumerator batchEnumerator; + + public T Current => batchEnumerator.Current; + + public XAsyncEnumeratorAdapter ( + IAsyncCursorSource asyncCursorSource, + CancellationToken cancellationToken + ) { + // + this.asyncCursorSource = asyncCursorSource; + this.cancellationToken = cancellationToken; + } + + public async ValueTask MoveNextAsync () { + // + if (asyncCursor == null) { + asyncCursor = await asyncCursorSource + .ToCursorAsync (cancellationToken); + } + + // + if (batchEnumerator != null && + batchEnumerator.MoveNext ()) { + return true; + } + + // + if (asyncCursor != null && + await asyncCursor.MoveNextAsync (cancellationToken)) { + // + batchEnumerator?.Dispose (); + batchEnumerator = asyncCursor.Current.GetEnumerator (); + + // + return batchEnumerator.MoveNext (); + } + + // + return false; + } + + public async ValueTask DisposeAsync () { + // + await Task.CompletedTask; + asyncCursor?.Dispose (); + asyncCursor = null; + } + } +} \ No newline at end of file diff --git a/Mongo/XMongoAsyncCursor.cs b/Mongo/XMongoAsyncCursor.cs new file mode 100644 index 0000000..ec75183 --- /dev/null +++ b/Mongo/XMongoAsyncCursor.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace xDataService.Mongo { + public class XMongoAsyncCursor : IAsyncCursor { + public IEnumerable Current { get; } + private IEnumerator CurrentEnumerator { get; } + + public XMongoAsyncCursor (IEnumerable source) { + this.Current = source; + this.CurrentEnumerator = source.GetEnumerator (); + } + + public void Dispose () { + CurrentEnumerator.Dispose (); + } + + public bool MoveNext (CancellationToken cancellationToken = default) { + return CurrentEnumerator.MoveNext (); + } + + public Task MoveNextAsync (CancellationToken cancellationToken = default) { + return Task.Run ( + () => CurrentEnumerator.MoveNext (), + cancellationToken : cancellationToken + ); + } + } +} \ No newline at end of file diff --git a/Mongo/XMongoQueryable.cs b/Mongo/XMongoQueryable.cs new file mode 100644 index 0000000..a99097c --- /dev/null +++ b/Mongo/XMongoQueryable.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using xModels.Base; + +namespace xDataService.Mongo { + public class XMongoQueryable : IMongoQueryable + where TEntity : XBaseEntity { + private readonly IMongoCollection collection; + + public Type ElementType { get; } + public IQueryable Items { get; } + public Expression Expression { get; } + public IQueryProvider Provider { get; } + + public XMongoQueryable ( + IQueryable items, + IMongoCollection collection + ) { + // + this.Items = items; + this.collection = collection; + + // + Provider = items.Provider; + Expression = items.Expression; + ElementType = items.ElementType; + } + + public IEnumerator GetEnumerator () { + return Items.GetEnumerator (); + } + + public QueryableExecutionModel GetExecutionModel () { + return collection.AsQueryable ().GetExecutionModel (); + } + + public IAsyncCursor ToCursor (CancellationToken cancellationToken = default) { + return new XMongoAsyncCursor (Items); + } + + public Task> ToCursorAsync (CancellationToken cancellationToken = default) { + return Task.Run (() => new XMongoAsyncCursor (Items) as IAsyncCursor); + } + + IEnumerator IEnumerable.GetEnumerator () { + return Items.GetEnumerator (); + } + } +} \ No newline at end of file diff --git a/Mongo/XMongoReadPreferenceResolver.cs b/Mongo/XMongoReadPreferenceResolver.cs new file mode 100644 index 0000000..f22dcbb --- /dev/null +++ b/Mongo/XMongoReadPreferenceResolver.cs @@ -0,0 +1,23 @@ +using MongoDB.Driver; + +namespace xDataService.Mongo { + public static class XMongoReadPreferenceResolver { + public static ReadPreference GetEffectiveReadPreference ( + IClientSessionHandle session, + ReadPreference explicitReadPreference, + ReadPreference defaultReadPreference) { + if (explicitReadPreference != null) { + return explicitReadPreference; + } + + if (session.IsInTransaction) { + var transactionReadPreference = session.WrappedCoreSession.CurrentTransaction.TransactionOptions.ReadPreference; + if (transactionReadPreference != null) { + return transactionReadPreference; + } + } + + return defaultReadPreference ?? ReadPreference.Primary; + } + } +} \ No newline at end of file diff --git a/MongoRepositories/XBaseMongoRepository.cs b/MongoRepositories/XBaseMongoRepository.cs new file mode 100644 index 0000000..4755989 --- /dev/null +++ b/MongoRepositories/XBaseMongoRepository.cs @@ -0,0 +1,1098 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using xCommons.Extensions; +using xDataService.Configuration; +using xDataService.Extensions; +using xDataService.Interfaces; +using xDataService.Models; +using xModels.Base; +using xModels.Dtos; + +namespace xDataService.MongoRepositories { + /// + /// Base MongoDb Base Entity Repository Pattern implementation ... + /// Only Used on MongoDb ... + /// + /// is the Entity type + /// is the Entity Key Type + /// is the DbContext Type + public abstract class XBaseMongoRepository : IXBaseRepository + where T : XBaseEntity { + // + private readonly string collectionName; + public List> bulkCollection; + public readonly IMongoCollection collection; + private readonly IXKeyGenerator keyGenerator; + public readonly XDataServiceConfiguration configuration; + private readonly IXBaseRepositoryEvents baseRepositoryEvents; + + // + public abstract IMongoQueryable GetFullDbSet (); + + // + #region Constructor ... + protected XBaseMongoRepository ( + XDataServiceConfiguration configuration, + string collectionName = null, + IXKeyGenerator keyGenerator = null, + IXBaseRepositoryEvents baseRepositoryEvents = null + ) { + // + this.keyGenerator = keyGenerator; + this.configuration = configuration; + this.collectionName = collectionName + .IsNullOrEmpty () ? + typeof (T).Name : + collectionName; + this.baseRepositoryEvents = baseRepositoryEvents; + + // + var client = new MongoClient (configuration.GetMongoDbURI ()); + var database = client.GetDatabase (configuration.GetMongoDbDatabase ()); + + // + bulkCollection = new List> (); + collection = database.GetCollection ( + this.collectionName + ); + } + #endregion + + // + #region Add ... + public async Task AddAsync ( + T item, + bool saveChanges = true + ) { + // + // Handle Key ... + item = await HandleKeyAsync (item); + + // + // Add Action model to Bulk Collection ... + bulkCollection.Add (new InsertOneModel (item)); + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .AddEvent (new XBaseEventModel (item)); + } + + // + // Return result base on action Succeed ... + return isSucceed ? + item : + null; + } + + public async Task AddOrUpdateAsync ( + T item, + bool saveChanges = true + ) { + // + var isExists = await IsExistsAsync (GetKey (item)); + if (!isExists) { + // + // Handle Key ... + item = await HandleKeyAsync (item); + + // + // Add Action model to Bulk Collection ... + bulkCollection + .Add (new InsertOneModel (item)); + } else { + // + // Filter ... + var filter = Builders.Filter.Eq (i => i.Id, item.Id); + + // + // Add Action model to Bulk Collection ... + bulkCollection + .Add (new ReplaceOneModel (filter, item) { IsUpsert = true }); + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + // + if (isExists) { + baseRepositoryEvents + .UpdateEvent (new XBaseEventModel (item)); + } else { + baseRepositoryEvents + .AddEvent (new XBaseEventModel (item)); + } + } + + // + // Return result base on action Succeed ... + return isSucceed ? + item : + null; + } + + public async Task AddRangeAsync ( + IEnumerable items, + bool saveChanges = true + ) { + // + foreach (var item in items) { + // + // Handle Key ... + var keyHandledItem = await HandleKeyAsync (item); + + // + // Add Action model to Bulk Collection ... + bulkCollection.Add (new InsertOneModel (keyHandledItem)); + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .AddManyEvent (new XBaseEventModel> (null)); + } + } + #endregion + + // + #region Remove ... + public async Task RemoveAsync ( + TKey id, + bool softDelete = true, + bool saveChanges = true + ) { + // + // Retrieve Item ... + var item = await GetAsync ( + id, + ignoreSoftDeleteds : softDelete + ); + if (item.IsNull ()) { + return null; + } + + // + // Filter ... + var filter = Builders.Filter.Eq (i => i.Id, id); + + // + // Check SoftDelete ... + if (softDelete && configuration.EnableSoftDelete) { + // + // Set Soft Delete ... + item.Deleted = true; + + // + // Update ... + // Add Action model to Bulk Collection ... + bulkCollection.Add (new ReplaceOneModel (filter, item) { IsUpsert = true }); + } else { + // + // Delete ... + // Add Action model to Bulk Collection ... + bulkCollection.Add (new DeleteOneModel (filter)); + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + // + baseRepositoryEvents + .RemoveEvent (new XBaseEventModel (item)); + } + + // + // Return result base on action Succeed ... + return isSucceed ? + item : + null; + } + + public async Task RemoveAsync ( + T item, + bool softDelete = true, + bool saveChanges = true + ) { + // + // Check item Exists ... + var isExists = await IsExistsAsync (item.Id); + if (item.IsNull ()) { + return null; + } + + // + // Filter ... + var filter = Builders.Filter.Eq (i => i.Id, item.Id); + + // + // Check SoftDelete ... + if (softDelete && configuration.EnableSoftDelete) { + // + // Set Soft Delete ... + item.Deleted = true; + + // + // Update ... + // Add Action model to Bulk Collection ... + bulkCollection.Add (new ReplaceOneModel (filter, item) { IsUpsert = true }); + } else { + // + // Delete ... + // Add Action model to Bulk Collection ... + bulkCollection.Add (new DeleteOneModel (filter)); + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + // + baseRepositoryEvents + .RemoveEvent (new XBaseEventModel (item)); + } + + // + // Return result base on action Succeed ... + return isSucceed ? + item : + null; + } + + public async Task RemoveRangeAsync ( + IEnumerable items, + bool softDelete = true, + bool saveChanges = true + ) { + // + // loop through items ... + foreach (var item in items) { + // + // Filter ... + var filter = Builders.Filter.Eq (i => i.Id, item.Id); + + // + // Check Soft Delete ... + if (softDelete && configuration.EnableSoftDelete) { + // + // Set Soft Delete ... + item.Deleted = true; + + // + // Update ... + // Add Action model to Bulk Collection ... + bulkCollection.Add (new ReplaceOneModel (filter, item) { IsUpsert = true }); + } else { + // + // Delete ... + // Add Action model to Bulk Collection ... + bulkCollection.Add (new DeleteOneModel (filter)); + } + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .RemoveManyEvent (new XBaseEventModel> (null)); + } + } + #endregion + + // + #region Retrieve ... + public IQueryable AsQueryable () { + return collection.AsQueryable (); + } + + public async Task GetAsync ( + TKey id, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ) { + // + // Retrieve Result ... + var result = await FindOneAsync ( + whereClause: x => x.Id + .ToString () + .ToNormalString () == id + .ToString () + .ToNormalString (), + ignoreSoftDeleteds : true, + containsDetail : containsDetail + ); + + // + await Task.CompletedTask; + + // + // Resturn Result ... + return result; + } + + public async Task> GetAllAsync ( + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ) { + // + var result = GetDbSet (containsDetail: containsDetail) + .AsQueryable () + .AsEnumerable (); + + // + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + result = result.Where (i => i.Deleted == false); + } + + // + await Task.CompletedTask; + + // + return result; + } + + public async Task FindOneAsync ( + Expression> whereClause, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ) { + // + // Generate Where Function ... + var whereFunc = whereClause.Compile (); + + // + // Handle Soft Deleted Items ... + Func ignoreSoftDeletedsWhereFunc = null; + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + // + Expression> ignoreSoftDeletedsWhereClause = x => x.Deleted == false; + ignoreSoftDeletedsWhereFunc = ignoreSoftDeletedsWhereClause.Compile (); + } + + // + // Get Enumerable ... + var enumerator = await GetAsyncEnumerable (containsDetail); + + // + T result = null; + await + foreach (var entity in enumerator) { + // + var isApproved = whereFunc (entity) && + (ignoreSoftDeletedsWhereFunc.IsNull () ? + true : + ignoreSoftDeletedsWhereFunc (entity)); + if (isApproved) { + // + result = entity; + break; + } + } + + // + return result; + } + + public async Task> FindManyAsync ( + Expression> whereClause, + bool ignoreSoftDeleteds = true, + bool containsDetail = false + ) { + // + // Generate Where Function ... + var whereFunc = whereClause.Compile (); + + // + // Handle Soft Deleted Items ... + Func ignoreSoftDeletedsWhereFunc = null; + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + // + Expression> ignoreSoftDeletedsWhereClause = x => x.Deleted == false; + ignoreSoftDeletedsWhereFunc = ignoreSoftDeletedsWhereClause.Compile (); + } + + // + // Get Enumerable ... + var enumerator = await GetAsyncEnumerable (containsDetail); + + // + var result = new List (); + await + foreach (var entity in enumerator) { + // + var isApproved = whereFunc (entity) && + (ignoreSoftDeletedsWhereFunc.IsNull () ? + true : + ignoreSoftDeletedsWhereFunc (entity)); + if (isApproved) { + result.Add (entity); + } + } + + // + return result.AsEnumerable (); + } + + public async Task> QueryAsync ( + XQuery query, + bool ignoreSoftDeleteds = true + ) { + // + if (query.PageSize < configuration + .PagingConfiguration + .MinAvailablePageSize) { + query.PageSize = configuration + .PagingConfiguration + .DefaultPageSize; + } + + // + if (query.PageSize > configuration + .PagingConfiguration + .MaxAvailablePageSize) { + query.PageSize = configuration + .PagingConfiguration + .MaxAvailablePageSize; + } + + // + // Where Filter Handler ... + Expression> whereClause = i => + i.PropValuesContains (query.Filter); + var whereFunc = whereClause.Compile (); + + // + // Handle Soft Deleted Items ... + Func ignoreSoftDeletedsWhereFunc = null; + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + // + Expression> ignoreSoftDeletedsWhereClause = x => x.Deleted == false; + ignoreSoftDeletedsWhereFunc = ignoreSoftDeletedsWhereClause.Compile (); + } + + // + // Get Enumerable ... + var enumerator = await GetAsyncEnumerable ( + containsDetail: query.ContainsDetail + ); + + // + // Count Total Items ... + var totalItemsCount = GetQueryable ( + containsDetail: query.ContainsDetail + ) + .Count (); + + // + var filteredItems = new List (); + await + foreach (var entity in enumerator) { + // + var isApproved = (query.Filter + .IsNullOrEmpty () ? + true : + whereFunc (entity) + ) && + (ignoreSoftDeletedsWhereFunc.IsNull () ? + true : + ignoreSoftDeletedsWhereFunc (entity)); + if (isApproved) { + filteredItems.Add (entity); + } + } + + // + // Count Filtered Items ... + int totalFilteredItemsCount = filteredItems.Count (); + + // + // Apply Paging ... + var items = filteredItems + .ApplyPaging ( + query.Page, + query.PageSize); + + // + // Apply Sorting ... + if (!query.SortBy.IsNullOrEmpty ()) { + items = items + .ApplySorting ( + query.SortBy, + query.IsAscending); + } + + // + // Generate Result Object ... + var queryResult = new XQueryResult { + Items = filteredItems.AsEnumerable (), + Page = query.Page, + PageSize = query.PageSize, + TotalItems = totalItemsCount, + TotalPages = await PagesCountAsync ( + query.PageSize, + totalFilteredItemsCount + ), + TotalFilteredItems = totalFilteredItemsCount + }; + + // + return queryResult; + } + + public async Task> ConditionalQueryAsync ( + Expression> whereClause, + XQuery query, + bool ignoreSoftDeleteds = true + ) { + // + if (query.PageSize < configuration + .PagingConfiguration + .MinAvailablePageSize) { + query.PageSize = configuration + .PagingConfiguration + .DefaultPageSize; + } + + // + if (query.PageSize > configuration + .PagingConfiguration + .MaxAvailablePageSize) { + query.PageSize = configuration + .PagingConfiguration + .MaxAvailablePageSize; + } + + // + // Generate Where Func ... + var whereFunc = whereClause.Compile (); + + // + // Where Filter Handler ... + Expression> whereFilterClause = i => + i.PropValuesContains (query.Filter); + var whereFilterFunc = whereClause.Compile (); + + // + // Handle Soft Deleted Items ... + Func ignoreSoftDeletedsWhereFunc = null; + if (ignoreSoftDeleteds && configuration.EnableSoftDelete) { + // + Expression> ignoreSoftDeletedsWhereClause = x => x.Deleted == false; + ignoreSoftDeletedsWhereFunc = ignoreSoftDeletedsWhereClause.Compile (); + } + + // + // Get Enumerable ... + var enumerator = await GetAsyncEnumerable ( + containsDetail: query.ContainsDetail + ); + + // + // Count Total Items ... + var totalItemsCount = GetQueryable ( + containsDetail: query.ContainsDetail + ) + .Count (); + + // + var filteredItems = new List (); + await + foreach (var entity in enumerator) { + // + var isApproved = whereFunc (entity) && + (query.Filter + .IsNullOrEmpty () ? + true : + whereFilterFunc (entity) + ) && + (ignoreSoftDeletedsWhereFunc.IsNull () ? + true : + ignoreSoftDeletedsWhereFunc (entity)); + if (isApproved) { + filteredItems.Add (entity); + } + } + + // + // Count Filtered Items ... + int totalFilteredItemsCount = filteredItems.Count (); + + // + // Apply Paging ... + var items = filteredItems + .ApplyPaging ( + query.Page, + query.PageSize); + + // + // Apply Sorting ... + if (!query.SortBy.IsNullOrEmpty ()) { + items = items + .ApplySorting ( + query.SortBy, + query.IsAscending); + } + + // + // Generate Result Object ... + var queryResult = new XQueryResult { + Items = filteredItems.AsEnumerable (), + Page = query.Page, + PageSize = query.PageSize, + TotalItems = totalItemsCount, + TotalPages = await PagesCountAsync ( + query.PageSize, + totalFilteredItemsCount + ), + TotalFilteredItems = totalFilteredItemsCount + }; + + // + return queryResult; + } + #endregion + + // + #region Update ... + public async Task UpdateAsync ( + TKey id, + T item, + bool saveChanges = true + ) { + // + // Filter ... + var filter = Builders.Filter.Eq (i => i.Id, id); + + // + // Add Action model to Bulk Collection ... + bulkCollection.Add (new ReplaceOneModel (filter, item) { IsUpsert = false }); + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .UpdateEvent (new XBaseEventModel (item)); + } + + // + // Return result base on action Succeed ... + return isSucceed ? + item : + null; + } + + public async Task UpdateRangeAsync ( + IEnumerable items, + bool saveChanges = true + ) { + // + // loop through items ... + foreach (var item in items) { + // + // Filter ... + var filter = Builders.Filter.Eq (i => i.Id, item.Id); + + // + // Add Action model to Bulk Collection ... + bulkCollection.Add (new ReplaceOneModel (filter, item) { IsUpsert = true }); + } + + // + // Check action is Succeeded or not ... + var isSucceed = false; + if (saveChanges) { + // + // Get Modified Count ... + var qResult = await SaveChangesAsync (); + + // + // set isSucceed Value based on Changes ... + isSucceed = qResult > 0; + } + + // + // Notify Event ... + if ( + isSucceed && + !baseRepositoryEvents.IsNull () + ) { + baseRepositoryEvents + .UpdateManyEvent (new XBaseEventModel> (null)); + } + + // + // Return result base on action Succeed ... + return isSucceed; + } + #endregion + + // + #region Count ... + public async Task CountAsync (bool ignoreSoftDeleteds = true) { + // + // Filter ... + var filter = Builders.Filter.Eq (i => i.Deleted, false); + + // + var result = ignoreSoftDeleteds && configuration.EnableSoftDelete ? (int) GetDbSet () + .Count (x => filter.Inject ()) : + (int) GetDbSet () + .Count (); + + // + await Task.CompletedTask; + + // + return result; + } + + public async Task PagesCountAsync ( + int pageSize, + int? totalItems = null + ) { + // + int count = totalItems.HasValue ? totalItems.Value : await CountAsync (); + int pagesCount = count / pageSize; + + // + if (count % pageSize > 0) { + pagesCount++; + } + + // + return pagesCount; + } + #endregion + + // + #region Exists ... + public async Task IsExistsAsync (TKey id, bool ignoreSoftDeleteds = true) { + // + var result = await FindOneAsync (i => + GetKey (i) + .ToString () == id + .ToString (), + ignoreSoftDeleteds : ignoreSoftDeleteds, + containsDetail : false + ); + + // + return !result.IsNull (); + } + #endregion + + // + #region Unit Of Work ... + public async Task SaveChangesAsync () { + // + try { + // + // Write all Bulk Models which stored inside bulkCollection ... + var result = await collection + .BulkWriteAsync (bulkCollection); + + // + // Clear Bulk Collection ... + bulkCollection.Clear (); + + // + // Return number of Modified Documents ... + var resultCount = (int) result.ModifiedCount + + (int) result.InsertedCount + + (int) result.DeletedCount; + + // + return resultCount; + } catch (Exception ex) { + // + // Log Thrown Exception ... + Console.WriteLine ($"XMongo Repository Exception: {ex.Message} ..."); + + // + // Return less than zero value ... + return -1; + } + } + #endregion + + // + #region Keys ... + public void SetKey ( + ref T item, + TKey id + ) { + // + var props = item.GetType ().GetProperties (); + var keyProp = props.FirstOrDefault (p => p.Name == nameof (XBaseEntity.Id)); + if (keyProp.IsNull ()) { + return; + } + + // + Type t = Nullable.GetUnderlyingType (keyProp.PropertyType) ?? keyProp.PropertyType; + object safeValue = (id == null) ? null : Convert.ChangeType (id, t); + keyProp.SetValue (item, safeValue, null); + } + + public TKey GetKey (T item) { + // + var props = item.GetType ().GetProperties (); + var keyProp = props.FirstOrDefault (p => p.Name == nameof (XBaseEntity.Id)); + + // + var keyString = string.Empty; + if (keyProp.IsNull ()) { + keyString = string.Empty; + } else { + keyString = keyProp.GetValue (item).ToString (); + } + + // + if (keyString.IsNullOrEmpty ()) { + return default (TKey); + } + + // + // Prevent Deserializing issues throug JsonReader ... + if (keyString.IsGuid () && typeof (TKey) == typeof (Guid)) { + return item.Id; + } + + // + return keyString.FromJSON (); + } + + public async Task HandleKeyAsync (T item) { + // + var keyType = typeof (TKey); + + // + // Handle Guid Key Type ... + if (!keyGenerator.IsNull () && + keyGenerator.IsEmpty (item.Id) + ) { + // + var newKey = await keyGenerator.GenerateKey (this); + + // + // InCrease Key if Type of TKey is Int and BulkDocs Contaisn Items ... + if (keyType == typeof (int)) { + // + var lastBulkedInsertedItem = bulkCollection + .Where (i => i.GetType () == typeof (InsertOneModel)) + .Select (i => (i as InsertOneModel).Document) + .OrderByDescending (nameof (XBaseEntity.Id)) + .FirstOrDefault (); + + // + // Renew Key if Exists inside bulkCollection ... + if (!lastBulkedInsertedItem.IsNull ()) { + // + var increasedKey = (Convert.ToInt32 (lastBulkedInsertedItem.Id)) + 1; + + // + newKey = Convert.ChangeType ( + increasedKey.ToDynamicObject (), + typeof (TKey) + ); + } + } + + // + SetKey (ref item, newKey); + } + + // + return item; + } + #endregion + + // + #region Detach ... + public void Detach (T item) { } + + public void Detach (IEnumerable items) { } + + public void Detach (XQueryResult query) { } + + public void Detach (XPageResponse page) { } + #endregion + + // + #region Others ... + public void Dispose () { } + + public string GetPropValues (T item) { + // + var props = item.GetType ().GetProperties (); + var vals = props.Select (p => p.GetValue (p.Name)); + + // + return vals.ToJSON (); + } + #endregion + + // + #region Private ... + private IMongoQueryable GetDbSet ( + bool containsDetail = false + ) { + // + IMongoQueryable result = null; + if (!containsDetail) { + result = collection + .AsQueryable (); + } else { + result = GetFullDbSet (); + } + + // + return result; + } + + private IMongoQueryable GetQueryable ( + bool containsDetail = false + ) { + return GetDbSet ( + containsDetail: containsDetail + ); + // .AsQueryable (); + } + + private async Task> GetAsyncCursor ( + bool containsDetail = false + ) { + return await GetQueryable ( + containsDetail: containsDetail + ) + .ToCursorAsync (); + } + + private async Task> GetAsyncEnumerable ( + bool containsDetail = false + ) { + return (await GetAsyncCursor ( + containsDetail: containsDetail + )) + .ToAsyncEnumerable (); + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XGuidKeyGenerator.cs b/Providers/XGuidKeyGenerator.cs new file mode 100644 index 0000000..9c0cc40 --- /dev/null +++ b/Providers/XGuidKeyGenerator.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using xCommons.Extensions; +using xDataService.Interfaces; +using xModels.Base; + +namespace xDataService.Providers { + public class XGuidKeyGenerator : IXKeyGenerator + where TEntity : XBaseEntity { + private readonly IXSequentialGuid sequentialGuid; + + public XGuidKeyGenerator ( + IXSequentialGuid sequentialGuid = null + ) { + this.sequentialGuid = sequentialGuid; + } + + public async Task GenerateKey ( + IXBaseRepository repository + ) { + // + await Task.CompletedTask; + + // + // Check if provided SequentialGuid ... + if (sequentialGuid.IsNull ()) { + return Guid.NewGuid (); + } else { + return sequentialGuid.Next (); + } + } + + public bool IsEmpty (Guid id) { + // + var result = id.IsNull () || id.IsDefaultGuid (); + + // + return result; + } + } +} \ No newline at end of file diff --git a/Providers/XIntKeyGenerator.cs b/Providers/XIntKeyGenerator.cs new file mode 100644 index 0000000..33b68d2 --- /dev/null +++ b/Providers/XIntKeyGenerator.cs @@ -0,0 +1,33 @@ +using System.Linq; +using System.Threading.Tasks; +using xCommons.Extensions; +using xDataService.Extensions; +using xDataService.Interfaces; +using xModels.Base; + +namespace xDataService.Providers { + public class XIntKeyGenerator : IXKeyGenerator + where TEntity : XBaseEntity { + public async Task GenerateKey (IXBaseRepository repository) { + // + var items = (await repository.GetAllAsync ()) + .OrderByDescending (nameof (XBaseEntity.Id)); + var last = items + .FirstOrDefault (); + + // + var result = last.IsNull () ? 1 : last.Id + 1; + + // + return result; + } + + public bool IsEmpty (int id) { + // + var result = id <= 0; + + // + return result; + } + } +} \ No newline at end of file diff --git a/Providers/XSequentialGuid.cs b/Providers/XSequentialGuid.cs new file mode 100644 index 0000000..b4e4423 --- /dev/null +++ b/Providers/XSequentialGuid.cs @@ -0,0 +1,61 @@ +using System; +using xDataService.Interfaces; + +namespace xDataService.Helpers { + /// + /// this service provide Sequential GUID mechanism for Entity Ids ... + /// + public class XSequentialGuid : IXSequentialGuid { + private static int[] sqlOrderMap = null; + private static int[] SQLORDERMAP { + get { + if (sqlOrderMap == null) { + sqlOrderMap = new int[16] { + 3, + 2, + 1, + 0, + 5, + 4, + 7, + 6, + 9, + 8, + 15, + 14, + 13, + 12, + 11, + 10 + }; + // 3 - the least significant byte in Guid ByteArray [for SQL Server ORDER BY clause] + // 10 - the most significant byte in Guid ByteArray [for SQL Server ORDERY BY clause] + } + return sqlOrderMap; + } + } + + private Guid currentGuid; + + public XSequentialGuid () { + currentGuid = Guid.NewGuid (); + } + + public Guid GetCurrentGuid () { + return currentGuid; + } + + public Guid Next () { + byte[] bytes = currentGuid.ToByteArray (); + for (int mapIndex = 0; mapIndex < 16; mapIndex++) { + int bytesIndex = SQLORDERMAP[mapIndex]; + bytes[bytesIndex]++; + if (bytes[bytesIndex] != 0) { + break; // No need to increment more significant bytes + } + } + currentGuid = new Guid (bytes); + return currentGuid; + } + } +} \ No newline at end of file diff --git a/Providers/XStringKeyGenerator.cs b/Providers/XStringKeyGenerator.cs new file mode 100644 index 0000000..f910d57 --- /dev/null +++ b/Providers/XStringKeyGenerator.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using xCommons.Extensions; +using xDataService.Interfaces; +using xModels.Base; + +namespace xDataService.Providers { + public class XStringKeyGenerator : IXKeyGenerator, string> { + private readonly IXSequentialGuid sequentialGuid; + + public XStringKeyGenerator (IXSequentialGuid sequentialGuid = null) { + this.sequentialGuid = sequentialGuid; + } + + public async Task GenerateKey (IXBaseRepository, string> repository) { + // + await Task.CompletedTask; + + // + if (sequentialGuid.IsNull ()) { + return Guid.NewGuid ().ToString (); + } else { + return sequentialGuid.Next ().ToString (); + } + } + + public bool IsEmpty (string id) { + return id.IsNullOrEmpty (); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e741bc3 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# xDataService + +it is a Part of xDashboard on SaherElm IT Center which provides: + +- all required Repositories, UnitOfWorks and other tools related to Data Manipulation in xApiGateway workspace. + +this module has following dependencies : + +- xModels +- xCommons + +for configure and use this Module refer to DI.XDIHelperExtension.cs file. + +## Implementation + +for using this modules and its provided tools and services for preparing your data provider usages + +## Maintainer + +Hadi Khazaee asl + +[https://www.saherelm.ir](https://www.saherelm.ir) + +[hadi_khazaee_asl@yahoo.com](mailto:hadi_khazaee_asl@yahoo.com) diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..87b6eb0 --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/xDataService.csproj b/xDataService.csproj new file mode 100644 index 0000000..64a4e92 --- /dev/null +++ b/xDataService.csproj @@ -0,0 +1,65 @@ + + + + + 8.0 + netstandard2.0 + xDashboard.xDataService + 1.0.0 + Hadi Khazaee Asl + SaherElm IT Center + + provide base requiremennts for Repository DataBase using and tools for using in xDashboard + project. + + + + icon.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + \ No newline at end of file