diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..577b8eb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,86 @@ + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf + +[*.cs] +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = when_multiline:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent +csharp_indent_labels = one_less_than_current +csharp_style_throw_expression = true:suggestion +csharp_space_around_binary_operators = before_and_after \ No newline at end of file diff --git a/Deveel.Repository.sln b/Deveel.Repository.sln index d692e2a..d0c2834 100644 --- a/Deveel.Repository.sln +++ b/Deveel.Repository.sln @@ -33,7 +33,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.EntityMod EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.DynamicLinq", "src\Deveel.Repository.DynamicLinq\Deveel.Repository.DynamicLinq.csproj", "{8B09EBB3-F8D5-483D-AFD6-FCCC5F186324}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Repository.MongoFramework.TestApi", "test\Deveel.Repository.MongoFramework.TestApi\Deveel.Repository.MongoFramework.TestApi.csproj", "{9B43B5FA-4990-4A34-B7A3-4996D9E93504}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.MongoFramework.TestApi", "test\Deveel.Repository.MongoFramework.TestApi\Deveel.Repository.MongoFramework.TestApi.csproj", "{9B43B5FA-4990-4A34-B7A3-4996D9E93504}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{603E47A0-B1D9-4B4D-AEA7-2E9272CE5C87}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Deveel.Repository.Core/Deveel.Repository.Core.csproj b/src/Deveel.Repository.Core/Deveel.Repository.Core.csproj index 5680a71..309fde9 100644 --- a/src/Deveel.Repository.Core/Deveel.Repository.Core.csproj +++ b/src/Deveel.Repository.Core/Deveel.Repository.Core.csproj @@ -42,8 +42,4 @@ - - - - diff --git a/src/Deveel.Repsotiory.MongoFramework/Data/EntityExtensions.cs b/src/Deveel.Repsotiory.MongoFramework/Data/EntityExtensions.cs deleted file mode 100644 index 1ccc06d..0000000 --- a/src/Deveel.Repsotiory.MongoFramework/Data/EntityExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -namespace Deveel.Data { - static class EntityExtensions { - private static bool TryGetMemberValue(this object entity, string memberName, [MaybeNullWhen(false)] out TValue value) { - var binding = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic; - var property = entity.GetType().GetProperty(memberName, binding); - if (property == null || !property.CanRead) { - value = default; - return false; - } - - var propValue = property.GetValue(entity, null); - if (propValue == null) { - value = default; - return false; - } - - if (propValue is TValue v) { - value = v; - return true; - } - - value = (TValue) Convert.ChangeType(propValue, typeof(TValue), CultureInfo.InvariantCulture); - return true; - } - - public static bool TryGetId(this object entity, [MaybeNullWhen(false)] out string? value) { - if (entity == null) { - value = null; - return false; - } - - if (entity is IDataEntity dataEntity) { - value = dataEntity.Id; - return true; - } - - if (TryGetMemberValue(entity, "Id", out var id)) { - value = id; - return true; - } - - value = null; - return false; - } - } -} diff --git a/src/Deveel.Repsotiory.MongoFramework/Data/MongoRepository.cs b/src/Deveel.Repsotiory.MongoFramework/Data/MongoRepository.cs index 910e025..2e270eb 100644 --- a/src/Deveel.Repsotiory.MongoFramework/Data/MongoRepository.cs +++ b/src/Deveel.Repsotiory.MongoFramework/Data/MongoRepository.cs @@ -9,11 +9,21 @@ using MongoFramework; using MongoFramework.Infrastructure; -using MongoFramework.Infrastructure.Commands; using MongoFramework.Infrastructure.Mapping; using MongoFramework.Linq; namespace Deveel.Data { + /// + /// An implementation of contract + /// that uses the MongoDB system to store and retrieve data. + /// + /// + /// The type of the that is used to + /// handling the connection to the MongoDB server. + /// + /// + /// The type of the entity that is stored in the repository. + /// public class MongoRepository : IRepository, IQueryableRepository, IPageableRepository, @@ -28,6 +38,15 @@ public class MongoRepository : IRepository, private IMongoDbSet? _dbSet; private bool disposed; + /// + /// Constructs the repository with the given context and logger. + /// + /// + /// The context that is used to handle the connection to the MongoDB server. + /// + /// + /// A logger instance that is used to log messages from the repository. + /// protected internal MongoRepository(TContext context, ILogger? logger = null) { Context = context; Logger = logger ?? NullLogger.Instance; @@ -36,17 +55,40 @@ protected internal MongoRepository(TContext context, ILogger? logger = null) { TenantId = tenantContext.TenantId; } + /// + /// Constructs the repository with the given context and logger. + /// + /// + /// The context that is used to handle the connection to the MongoDB server. + /// + /// + /// A logger instance that is used to log messages from the repository. + /// public MongoRepository(TContext context, ILogger>? logger = null) : this(context, (ILogger?)logger) { } - + /// + /// Gets the context that is used to handle the connection to the MongoDB server. + /// protected TContext Context { get; } + /// + /// Gets the that is used to handle the + /// repository operations. + /// protected IMongoDbSet DbSet => GetEntitySet(); + /// + /// Gets the instance that is used to log messages + /// protected ILogger Logger { get; } + /// + /// When the underlying context is a , + /// this property returns the tenant identifier that is used to filter + /// the data in the repository. + /// protected string? TenantId { get; } string? IMultiTenantRepository.TenantId => TenantId; @@ -55,6 +97,10 @@ public MongoRepository(TContext context, ILogger typeof(TEntity); + /// + /// Gets the instance that is used + /// to handle the data in the repository. + /// protected IMongoCollection Collection { get { var entityDef = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); @@ -75,13 +121,25 @@ private static string RequireString(object value) { return result; } + /// + /// Throws an exception if the repository has been disposed. + /// + /// + /// Thrown when the repository has been disposed. + /// protected void ThrowIfDisposed() { if (disposed) throw new ObjectDisposedException(GetType().Name); } + /// + /// Constructs a new that is + /// coherent with the context and the entity type. + /// + /// + /// protected virtual IMongoDbSet MakeEntitySet() { - if (Context is MongoDbTenantContext tenantContext && + if (Context is IMongoDbTenantContext tenantContext && typeof(IHaveTenantId).IsAssignableFrom(typeof(TEntity))) { var dbSetType = typeof(MongoDbTenantSet<>).MakeGenericType(typeof(TEntity)); var result = (IMongoDbSet?)Activator.CreateInstance(dbSetType, new object[] { Context }); @@ -105,7 +163,8 @@ private IMongoDbSet GetEntitySet() { string? IRepository.GetEntityId(object entity) => ((IRepository)this).GetEntityId(Assert(entity)); string? IRepository.GetEntityId(TEntity entity) { - var value = GetIdValue(entity); + var value = GetEntityId(entity); + if (value == null) return null; @@ -117,10 +176,46 @@ private IMongoDbSet GetEntitySet() { return Convert.ToString(value, CultureInfo.InvariantCulture); } - protected virtual object? GetIdValue(TEntity entity) - => entity.TryGetId(out var id) ? GetIdValue(id) : null; + /// + /// Gets the value of the ID property of the given entity. + /// + /// + /// The entity whose ID property value is to be retrieved. + /// + /// + /// Returns the value of the ID property of the given entity. + /// + protected virtual object? GetEntityId(TEntity entity) { + var entityDef = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); + + var idProperty = entityDef.GetIdProperty(); + + if (idProperty == null) + throw new RepositoryException($"The type '{typeof(TEntity)}' has no ID property specified"); - protected virtual object? GetIdValue(string? id) { + return idProperty.PropertyInfo.GetValue(entity); + } + + /// + /// Converts the given string value to the type of the ID property of the + /// entity managed by this repository. + /// + /// + /// The string representation of the ID value. + /// + /// + /// Returns the value converted accordingly to the type of the ID property + /// of the entity managed by this repository, or null if the given + /// string is null or empty. + /// + /// + /// Thrown if the entity managed by this repository has no ID property + /// + /// + /// Thrown when the value cannot be converted to the type of the ID + /// property of the entity managed by this repository. + /// + protected virtual object? ConvertIdValue(string? id) { if (String.IsNullOrWhiteSpace(id)) return null; @@ -145,6 +240,20 @@ private IMongoDbSet GetEntitySet() { throw new NotSupportedException($"It is not possible to convert the ID to '{valueType}'"); } + /// + /// Asserts that the given entity is of the type managed by this repository. + /// + /// + /// The object that has to be asserted. + /// + /// + /// Returns an instance of the object casted to the type managed by this + /// repository. + /// + /// + /// Thrown when the given entity is not of the type managed by this + /// repository + /// protected static TEntity Assert(object entity) { if (!(entity is TEntity entityObj)) throw new ArgumentException($"The type '{entity.GetType()}' is not assignable from '{typeof(TEntity)}'"); @@ -152,6 +261,19 @@ protected static TEntity Assert(object entity) { return entityObj; } + /// + /// Gets the MongoDB filter definition for the given query filter. + /// + /// + /// The query filter to be converted to a MongoDB filter definition. + /// + /// + /// Returns an instance of that + /// is mapped from the given query filter. + /// + /// + /// Thrown when the given query filter is not supported by this repository. + /// protected virtual FilterDefinition GetFilterDefinition(IQueryFilter? filter) { if (filter == null || filter.IsEmpty()) return Builders.Filter.Empty; @@ -174,6 +296,20 @@ protected virtual Expression> Field(string fieldName) { #region Controllable + /// + /// Verifies if the repository exists in the underlying database. + /// + /// + /// A cancellation token that can be used to cancel the operation. + /// + /// + /// Returns true if the repository exists in the underlying + /// database, otherwise false. + /// + /// + /// Thrown when an error occurs while verifying the existence of the + /// collection in the underlying database. + /// public async Task ExistsAsync(CancellationToken cancellationToken = default) { try { var entityDef = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); @@ -219,10 +355,35 @@ public async Task DropAsync(CancellationToken cancellationToken = default) { #region Create + /// + /// A callback method that is invoked before the entity is created. + /// + /// + /// The entity that is about to be created. + /// + /// + /// Returns the entity that is about to be created. + /// protected virtual TEntity OnCreating(TEntity entity) { return entity; } + /// + /// Creates a new entity in the repository. + /// + /// + /// The entity to be created in the repository. + /// + /// + /// A cancellation token that can be used to cancel the operation. + /// + /// + /// Returns the unique identifier of the created entity. + /// + /// + /// Thrown when an error occurs while creating the entity in the + /// underlying database. + /// public async Task CreateAsync(TEntity entity, CancellationToken cancellationToken = default) { ThrowIfDisposed(); cancellationToken.ThrowIfCancellationRequested(); @@ -240,7 +401,7 @@ public async Task CreateAsync(TEntity entity, CancellationToken cancella await DbSet.Context.SaveChangesAsync(cancellationToken); // TODO: this is UGLY! change the IRepository to use object keys instead? - var id = RequireString(GetIdValue(entity)); + var id = RequireString(GetEntityId(entity)); if (!String.IsNullOrWhiteSpace(TenantId)) { Logger.TraceCreatedForTenant(TenantId, id); @@ -272,7 +433,7 @@ public async Task> CreateAsync(IEnumerable entities, Canc await DbSet.Context.SaveChangesAsync(cancellationToken); // TODO: this is UGLY! Change the IRepository to work with object keys - return entities.Select(x => RequireString(GetIdValue(x))).ToList(); + return entities.Select(x => RequireString(GetEntityId(x))).ToList(); } catch (Exception ex) { Logger.LogUnknownError(ex); throw new RepositoryException("Could not add the list of entities", ex); @@ -283,7 +444,7 @@ public async Task> CreateAsync(IEnumerable entities, Canc #region Update - protected TEntity OnUpdating(TEntity entity) { + protected virtual TEntity OnUpdating(TEntity entity) { return entity; } @@ -294,7 +455,7 @@ public async Task UpdateAsync(TEntity entity, CancellationToken cancellati ThrowIfDisposed(); cancellationToken.ThrowIfCancellationRequested(); - var id = GetIdValue(entity); + var id = GetEntityId(entity); if (id == null) throw new ArgumentException(nameof(entity), "Cannot determine the identifier of the entity"); @@ -346,7 +507,7 @@ public async Task DeleteAsync(TEntity entity, CancellationToken cancellati ThrowIfDisposed(); cancellationToken.ThrowIfCancellationRequested(); - var entityId = GetIdValue(entity); + var entityId = GetEntityId(entity); if (entityId == null) throw new ArgumentException("The entity does not have an ID", nameof(entity)); @@ -400,7 +561,7 @@ Task IRepository.DeleteAsync(object entity, CancellationToken cancellation } try { - var idValue = GetIdValue(id); + var idValue = ConvertIdValue(id); var entry = Context.ChangeTracker.GetEntryById(idValue); if (entry != null && entry.State == EntityEntryState.Deleted) diff --git a/src/Deveel.Repsotiory.MongoFramework/Data/VersionUtil.cs b/src/Deveel.Repsotiory.MongoFramework/Data/VersionUtil.cs deleted file mode 100644 index f508918..0000000 --- a/src/Deveel.Repsotiory.MongoFramework/Data/VersionUtil.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; - -namespace Deveel.Data { - static class VersionUtil { - public static VersionFormat GetFormat(Type propertyType) { - if (propertyType == typeof(Guid) || - propertyType == typeof(string)) - return VersionFormat.Guid; - if (propertyType == typeof(int) || - propertyType == typeof(long)) - return VersionFormat.Integer; - - throw new NotSupportedException($"The type '{propertyType}' is not valid to handle versions"); - } - - private static object FormatInteger(object value) { - if (value is int i) - return i; - if (value is long l) - return l; - - if (value is string s1 && Int64.TryParse(s1, out var l2)) - return l2; - if (value is string s2 && Int32.TryParse(s2, out var i2)) - return i2; - - throw new NotSupportedException($"The value {value} is not a valid version integer"); - } - - private static object FormatGuid(object value) { - if (value is Guid guid) - return guid; - if (value is string s) - return s; - - throw new NotSupportedException($"The value {value} is not a valid version GUID"); - } - - public static object Format(object value, VersionFormat format) { - if (format == VersionFormat.Integer) { - return FormatInteger(value); - } else if (format == VersionFormat.Guid) { - return FormatGuid(value); - } - - throw new NotSupportedException(); - } - - public static object GetNewVersion(VersionFormat format, Type propertyType, object currentValue) { - if (format == VersionFormat.Guid) - return GetNewGuid(propertyType); - if (format == VersionFormat.Integer) - return GetNewInteger(propertyType, currentValue); - - throw new NotSupportedException(); - } - - private static object GetNewGuid(Type propertyType) { - if (propertyType == typeof(Guid)) - return Guid.NewGuid(); - if (propertyType == typeof(string)) - return Guid.NewGuid().ToString("N"); - - throw new NotSupportedException(); - } - - private static object GetNewInteger(Type propertyType, object currentValue) { - if (propertyType == typeof(int)) { - if (currentValue == null) - return 0; - if (currentValue is int i) - return i + 1; - if (currentValue is long l) - return (int) l + 1; - } - - if (propertyType == typeof(long)) { - if (currentValue == null) - return 0L; - if (currentValue is int i) - return (long)(i + 1); - if (currentValue is long l) - return l + 1; - } - - throw new NotSupportedException("Unable to increment a non integer"); - } - } -} diff --git a/src/Deveel.Repsotiory.MongoFramework/Deveel.Repository.MongoFramework.xml b/src/Deveel.Repsotiory.MongoFramework/Deveel.Repository.MongoFramework.xml index 769e3f4..582a8d4 100644 --- a/src/Deveel.Repsotiory.MongoFramework/Deveel.Repository.MongoFramework.xml +++ b/src/Deveel.Repsotiory.MongoFramework/Deveel.Repository.MongoFramework.xml @@ -25,5 +25,96 @@ This API supports the logging infrastructure and is not intended to be used directly from your code. It is subject to change in the future. + + + An implementation of contract + that uses the MongoDB system to store and retrieve data. + + + The type of the that is used to + handling the connection to the MongoDB server. + + + The type of the entity that is stored in the repository. + + + + + Constructs the repository with the given context and logger. + + + The context that is used to handle the connection to the MongoDB server. + + + A logger instance that is used to log messages from the repository. + + + + + Constructs the repository with the given context and logger. + + + The context that is used to handle the connection to the MongoDB server. + + + A logger instance that is used to log messages from the repository. + + + + + Gets the context that is used to handle the connection to the MongoDB server. + + + + + Gets the that is used to handle the + repository operations. + + + + + Gets the instance that is used to log messages + + + + + When the underlying context is a , + this property returns the tenant identifier that is used to filter + the data in the repository. + + + + + Gets the instance that is used + to handle the data in the repository. + + + + + Throws an exception if the repository has been disposed. + + + Thrown when the repository has been disposed. + + + + + Constructs a new that is + coherent with the context and the entity type. + + + + + + + Gets the value of the ID property of the given entity. + + + The entity whose ID property value is to be retrieved. + + + Returns the value of the ID property of the given entity. + +