diff --git a/Achilles.Entities.Sqlite.sln b/Achilles.Entities.Sqlite.sln index 935b5b7..12af74a 100644 --- a/Achilles.Entities.Sqlite.sln +++ b/Achilles.Entities.Sqlite.sln @@ -8,6 +8,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1F41A4A4-7A97-4704-BCCA-FA13DCC6BD3C}" ProjectSection(SolutionItems) = preProject achilles-icon.png = achilles-icon.png + Conduct.md = Conduct.md + Contribute.md = Contribute.md LICENSE.txt = LICENSE.txt NuGet.config = NuGet.config README.md = README.md diff --git a/Achilles.Entities.Sqlite/Achilles.Entities.Sqlite.csproj b/Achilles.Entities.Sqlite/Achilles.Entities.Sqlite.csproj index e522f83..fa67671 100644 --- a/Achilles.Entities.Sqlite/Achilles.Entities.Sqlite.csproj +++ b/Achilles.Entities.Sqlite/Achilles.Entities.Sqlite.csproj @@ -25,6 +25,7 @@ + diff --git a/Achilles.Entities.Sqlite/EntitySet.cs b/Achilles.Entities.Sqlite/EntitySet.cs index a7a1af1..c5d6145 100644 --- a/Achilles.Entities.Sqlite/EntitySet.cs +++ b/Achilles.Entities.Sqlite/EntitySet.cs @@ -11,6 +11,10 @@ #region Namespaces using Achilles.Entities.Linq; +using Remotion.Linq.EagerFetching.Parsing; +using Remotion.Linq.Parsing.ExpressionVisitors.Transformation; +using Remotion.Linq.Parsing.Structure; +using Remotion.Linq.Parsing.Structure.NodeTypeProviders; using System; using System.Collections; using System.Collections.Generic; @@ -96,7 +100,22 @@ private EntityQueryable EntityQueryable { if ( _entityQueryable == null ) { - _entityQueryable = new EntityQueryable( _context ); + var customNodeTypeRegistry = new MethodInfoBasedNodeTypeRegistry(); + + customNodeTypeRegistry.Register( new[] { typeof( EagerFetchingExtensions ).GetMethod( "FetchOne" ) }, typeof( FetchOneExpressionNode ) ); + customNodeTypeRegistry.Register( new[] { typeof( EagerFetchingExtensions ).GetMethod( "FetchMany" ) }, typeof( FetchManyExpressionNode ) ); + customNodeTypeRegistry.Register( new[] { typeof( EagerFetchingExtensions ).GetMethod( "ThenFetchOne" ) }, typeof( ThenFetchOneExpressionNode ) ); + customNodeTypeRegistry.Register( new[] { typeof( EagerFetchingExtensions ).GetMethod( "ThenFetchMany" ) }, typeof( ThenFetchManyExpressionNode ) ); + + var nodeTypeProvider = ExpressionTreeParser.CreateDefaultNodeTypeProvider(); + nodeTypeProvider.InnerProviders.Add( customNodeTypeRegistry ); + + var transformerRegistry = ExpressionTransformerRegistry.CreateDefault(); + var processor = ExpressionTreeParser.CreateDefaultProcessor( transformerRegistry ); + var expressionTreeParser = new ExpressionTreeParser( nodeTypeProvider, processor ); + var queryParser = new QueryParser( expressionTreeParser ); + + _entityQueryable = new EntityQueryable( _context, queryParser ); } return _entityQueryable; diff --git a/Achilles.Entities.Sqlite/Extensions/StringBuilderExtensions.cs b/Achilles.Entities.Sqlite/Extensions/StringBuilderExtensions.cs index 8476c55..9d97cb0 100644 --- a/Achilles.Entities.Sqlite/Extensions/StringBuilderExtensions.cs +++ b/Achilles.Entities.Sqlite/Extensions/StringBuilderExtensions.cs @@ -58,7 +58,27 @@ public static void AppendEnumerable( this StringBuilder stringBuilder, IEnumerab } else { - stringBuilder.AppendFormat( "{0}{1}", delimiter, item ); + stringBuilder.AppendFormat( "{0}{1}{2}", delimiter, prefix, item ); + } + } + } + + public static void AppendEnumerable( this StringBuilder stringBuilder, IEnumerable e, string prefix, string delimiter, string alias ) + { + bool first = true; + + foreach ( var item in e ) + { + if ( first ) + { + first = false; + stringBuilder.AppendFormat( "{0}{1}", prefix, item ); + stringBuilder.AppendFormat( " AS {0}_{1}", alias, item ); + } + else + { + stringBuilder.AppendFormat( "{0} {1}{2}", delimiter, prefix, item ); + stringBuilder.AppendFormat( " AS {0}_{1}", alias, item ); } } } diff --git a/Achilles.Entities.Sqlite/Linq/AsyncQueryableExtensions.cs b/Achilles.Entities.Sqlite/Linq/AsyncQueryableExtensions.cs index a9e422c..a478265 100644 --- a/Achilles.Entities.Sqlite/Linq/AsyncQueryableExtensions.cs +++ b/Achilles.Entities.Sqlite/Linq/AsyncQueryableExtensions.cs @@ -29,20 +29,6 @@ namespace Achilles.Entities.Linq { public static class AsyncQueryableExtensions { - public static IJoinQueryable SelectRelated( - this IQueryable source, - Expression> predicate, - CancellationToken cancellationToken = default ) - where TEntity : class - { - if ( source.Provider is IAsyncQueryProvider provider ) - { - throw new NotImplementedException(); - } - - throw new InvalidOperationException( "Provider is not async" ); - } - public static Task> ToListAsync( this IQueryable source, CancellationToken cancellationToken = default ) diff --git a/Achilles.Entities.Sqlite/Linq/EagerFetchingExtensions.cs b/Achilles.Entities.Sqlite/Linq/EagerFetchingExtensions.cs new file mode 100644 index 0000000..b59e440 --- /dev/null +++ b/Achilles.Entities.Sqlite/Linq/EagerFetchingExtensions.cs @@ -0,0 +1,110 @@ +#region Copyright Notice + +// Copyright (c) by Achilles Software, All rights reserved. +// +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// Send questions regarding this copyright notice to: mailto:todd.thomson@achilles-software.com + +#endregion + +#region Namespaces + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +#endregion + +namespace Achilles.Entities.Linq +{ + public static class EagerFetchingExtensions + { + public static FluentFetchRequest FetchMany( + this IQueryable query, + Expression>> relatedObjectSelector ) + { + if ( query == null ) + { + throw new ArgumentNullException( nameof( query ) ); + } + + if ( relatedObjectSelector == null ) + { + throw new ArgumentNullException( nameof( relatedObjectSelector ) ); + } + + var methodInfo = ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod( typeof( TOriginating ), typeof( TRelated ) ); + + return CreateFluentFetchRequest( methodInfo, query, relatedObjectSelector ); + } + + public static FluentFetchRequest FetchOne( + this IQueryable query, + Expression> relatedObjectSelector ) + { + if ( query == null ) + { + throw new ArgumentNullException( nameof( query ) ); + } + if ( relatedObjectSelector == null ) + { + throw new ArgumentNullException( nameof( relatedObjectSelector ) ); + } + + var methodInfo = ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod( typeof( TOriginating ), typeof( TRelated ) ); + + return CreateFluentFetchRequest( methodInfo, query, relatedObjectSelector ); + } + + public static FluentFetchRequest ThenFetchMany( + this FluentFetchRequest query, + Expression>> relatedObjectSelector ) + { + if ( query == null ) + { + throw new ArgumentNullException( nameof( query ) ); + } + if ( relatedObjectSelector == null ) + { + throw new ArgumentNullException( nameof( relatedObjectSelector ) ); + } + + var methodInfo = ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod( typeof( TQueried ), typeof( TFetch ), typeof( TRelated ) ); + + return CreateFluentFetchRequest( methodInfo, query, relatedObjectSelector ); + } + + public static FluentFetchRequest ThenFetchOne( + this FluentFetchRequest query, + Expression> relatedObjectSelector ) + { + if ( query == null ) + { + throw new ArgumentNullException( nameof( query ) ); + } + if ( relatedObjectSelector == null ) + { + throw new ArgumentNullException( nameof( relatedObjectSelector ) ); + } + + var methodInfo = ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod( typeof( TQueried ), typeof( TFetch ), typeof( TRelated ) ); + + return CreateFluentFetchRequest( methodInfo, query, relatedObjectSelector ); + } + + private static FluentFetchRequest CreateFluentFetchRequest( + MethodInfo currentFetchMethod, + IQueryable query, + LambdaExpression relatedObjectSelector ) + { + var queryProvider = query.Provider; // ArgumentUtility.CheckNotNullAndType( "query.Provider", query.Provider ); + + var callExpression = Expression.Call( currentFetchMethod, query.Expression, relatedObjectSelector ); + + return new FluentFetchRequest( queryProvider, callExpression ); + } + } +} diff --git a/Achilles.Entities.Sqlite/Linq/EntityQueryable.cs b/Achilles.Entities.Sqlite/Linq/EntityQueryable.cs index 21c330f..add380a 100644 --- a/Achilles.Entities.Sqlite/Linq/EntityQueryable.cs +++ b/Achilles.Entities.Sqlite/Linq/EntityQueryable.cs @@ -15,8 +15,8 @@ public class EntityQueryable : QueryableBase , IEntityAsyncQue { #region Constructor(s) - public EntityQueryable( DataContext context ) - : base( new EntityQueryProvider( context, typeof( EntityQueryable<> ), QueryParser.CreateDefault(), new EntityQueryExecutor( context ) ) ) + public EntityQueryable( DataContext context, QueryParser queryParser ) + : base( new EntityQueryProvider( context, typeof( EntityQueryable<> ), queryParser, new EntityQueryExecutor( context ) ) ) { } diff --git a/Achilles.Entities.Sqlite/Linq/ExpressionVisitors/SelectExpressionVisitor.cs b/Achilles.Entities.Sqlite/Linq/ExpressionVisitors/SelectExpressionVisitor.cs index a8fc957..f8f6bb4 100644 --- a/Achilles.Entities.Sqlite/Linq/ExpressionVisitors/SelectExpressionVisitor.cs +++ b/Achilles.Entities.Sqlite/Linq/ExpressionVisitors/SelectExpressionVisitor.cs @@ -1,4 +1,14 @@ -#region Namespaces +#region Copyright Notice + +// Copyright (c) by Achilles Software, All rights reserved. +// +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// Send questions regarding this copyright notice to: mailto:Todd.Thomson@achilles-software.com + +#endregion + +#region Namespaces using Achilles.Entities.Extensions; using Achilles.Entities.Relational; @@ -10,36 +20,68 @@ namespace Achilles.Entities.Linq.ExpressionVisitors { + /// + /// + /// public class SelectExpressionVisitor : SqlExpressionVisitor { + #region Fields + + private bool _inProjection = false; + private MemberAssignment _projectionBinding; + + #endregion + + #region Constructor(s) + public SelectExpressionVisitor( DataContext dbContext, SqlParameterCollection parameters ) : base( dbContext, parameters ) { } + #endregion + public static string GetStatement( DataContext dbContext, SqlParameterCollection parameters, Expression expression ) { - var expressionVisitor = new SelectExpressionVisitor( dbContext, parameters ); + var selectExpressionVisitor = new SelectExpressionVisitor( dbContext, parameters ); - expressionVisitor.Visit( expression ); + selectExpressionVisitor.Visit( expression ); - return expressionVisitor.GetStatement(); + return selectExpressionVisitor.GetStatement(); } protected override Expression VisitQuerySourceReference( QuerySourceReferenceExpression expression ) { var EntityMapping = _dbContext.Model.GetEntityMapping( expression.ReferencedQuerySource.ItemType ); - // Review: Check for null? - Statement.AppendEnumerable( - EntityMapping.ColumnMappings.Select( p => p.ColumnName ), - string.Format( "{0}.", - expression.ReferencedQuerySource.ItemName ), - string.Format( ", {0}.", expression.ReferencedQuerySource.ItemName ) ); + if ( _inProjection ) + { + Statement.AppendEnumerable( + EntityMapping.ColumnMappings.Select( p => p.ColumnName ), + string.Format( "{0}.", expression.ReferencedQuerySource.ItemName ), + ", ", + _projectionBinding.Member.Name ); + + } + else + { + Statement.AppendEnumerable( + EntityMapping.ColumnMappings.Select( p => p.ColumnName ), + string.Format( "{0}.", expression.ReferencedQuerySource.ItemName ), + ", " ); + } return expression; } + /// + /// The MemberInit expression creates a new object and initializes the object properties through the expression bindings. + /// + /// + /// select new EntityType { } + /// + /// The MemberInit expression. + /// The MemberInit expression parameter. protected override Expression VisitMemberInit( MemberInitExpression expression ) { for ( int i = 0; i < expression.Bindings.Count; i++ ) @@ -56,9 +98,22 @@ protected override Expression VisitMemberInit( MemberInitExpression expression ) Statement.Append( ", " ); } + if ( binding.Expression.NodeType == ExpressionType.Extension ) + { + _inProjection = true; + _projectionBinding = binding; + } + else + { + _inProjection = false; + } + Visit( binding.Expression ); - Statement.AppendFormat( " AS {0}", binding.Member.Name ); + if ( binding.Expression.NodeType == ExpressionType.MemberAccess ) + { + Statement.AppendFormat( " AS {0}", binding.Member.Name ); + } } return expression; diff --git a/Achilles.Entities.Sqlite/Linq/ExpressionVisitors/SqlExpressionVisitor.cs b/Achilles.Entities.Sqlite/Linq/ExpressionVisitors/SqlExpressionVisitor.cs index 33765d7..8d52636 100644 --- a/Achilles.Entities.Sqlite/Linq/ExpressionVisitors/SqlExpressionVisitor.cs +++ b/Achilles.Entities.Sqlite/Linq/ExpressionVisitors/SqlExpressionVisitor.cs @@ -174,6 +174,11 @@ protected override Expression VisitQuerySourceReference( QuerySourceReferenceExp return expression; } + //protected override Expression VisitSubQuery( SubQueryExpression expression ) + //{ + // return base.VisitSubQuery( expression ); + //} + protected override Expression VisitMember( MemberExpression expression ) { Visit( expression.Expression ); diff --git a/Achilles.Entities.Sqlite/Linq/FluentFetchRequest.cs b/Achilles.Entities.Sqlite/Linq/FluentFetchRequest.cs new file mode 100644 index 0000000..61f9ab6 --- /dev/null +++ b/Achilles.Entities.Sqlite/Linq/FluentFetchRequest.cs @@ -0,0 +1,28 @@ +#region Copyright Notice + +// Copyright (c) by Achilles Software, All rights reserved. +// +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// Send questions regarding this copyright notice to: mailto:todd.thomson@achilles-software.com + +#endregion + +#region Namespaces + +using Remotion.Linq; +using System.Linq; +using System.Linq.Expressions; + +#endregion + +namespace Achilles.Entities.Linq +{ + public class FluentFetchRequest : QueryableBase + { + public FluentFetchRequest( IQueryProvider provider, Expression expression ) + : base( provider, expression ) + { + } + } +} diff --git a/Achilles.Entities.Sqlite/Linq/SqliteQueryModelVisitor.cs b/Achilles.Entities.Sqlite/Linq/SqliteQueryModelVisitor.cs index 53dd305..c9ac523 100644 --- a/Achilles.Entities.Sqlite/Linq/SqliteQueryModelVisitor.cs +++ b/Achilles.Entities.Sqlite/Linq/SqliteQueryModelVisitor.cs @@ -6,6 +6,7 @@ using Remotion.Linq; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.ResultOperators; +using Remotion.Linq.EagerFetching; using System; using System.Collections.Generic; using System.Text; @@ -85,14 +86,29 @@ public override void VisitMainFromClause( MainFromClause fromClause, QueryModel fromClause.ItemName= fromClause.ItemName.Replace( "_", fromTableName + "_" ); } - _fromPart = string.Format( "{0} as {1}", fromTableName , fromClause.ItemName ); + SubQueryFromClauseModelVisitor.Visit( _context, _parameters, queryModel ); + + _fromPart = string.Format( "{0} AS {1}", fromTableName , fromClause.ItemName ); base.VisitMainFromClause( fromClause, queryModel ); } + public override void VisitAdditionalFromClause( AdditionalFromClause fromClause, QueryModel queryModel, int index ) + { + base.VisitAdditionalFromClause( fromClause, queryModel, index ); + } + public override void VisitResultOperator( ResultOperatorBase resultOperator, QueryModel queryModel, int index ) { - if ( resultOperator is SumResultOperator ) + if ( resultOperator is FetchOneRequest ) + { + ProcessFetchRequest( resultOperator as FetchRequestBase, queryModel ); + } + else if ( resultOperator is FetchManyRequest ) + { + ProcessFetchRequest( resultOperator as FetchRequestBase, queryModel ); + } + else if ( resultOperator is SumResultOperator ) { _selectPart = string.Format( "SUM({0})", _selectPart ); } @@ -184,5 +200,29 @@ public override void VisitJoinClause( JoinClause joinClause, QueryModel queryMod base.VisitJoinClause( joinClause, queryModel, index ); } + + private void ProcessFetchRequest( FetchRequestBase fetchRequest, QueryModel queryModel ) + { + var declaringType = fetchRequest.RelationMember.DeclaringType; + + // TODO: + + // INNER JOIN Vs LEFT JOIN (Perhaps use a FetchRequired for optimized INNER JOIN. + // Look at Required flag on Entity. + + // AutoMapper can use entity prefix underscore to map related entities from fetch. + + // How to update the SELECT for the fetched entity = use a join test to see how it works. + + // Want something like below: + + const string sql = @"SELECT tc.[ContactID] as ContactID, + tc.[ContactName] as ContactName + ,tp.[PhoneId] AS TestPhones_PhoneId + ,tp.[ContactId] AS TestPhones_ContactId + ,tp.[Number] AS TestPhones_Number + FROM TestContact tc + INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId"; + } } } diff --git a/Achilles.Entities.Sqlite/Linq/SubQueryFromClauseModelVisitor.cs b/Achilles.Entities.Sqlite/Linq/SubQueryFromClauseModelVisitor.cs new file mode 100644 index 0000000..ff9c845 --- /dev/null +++ b/Achilles.Entities.Sqlite/Linq/SubQueryFromClauseModelVisitor.cs @@ -0,0 +1,102 @@ +#region Namespaces + +using Achilles.Entities.Relational; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.EagerFetching; +using System.Collections.Generic; +using System.Linq.Expressions; + +#endregion + +namespace Achilles.Entities.Linq.ExpressionVisitors +{ + class SubQueryFromClauseModelVisitor : QueryModelVisitorBase + { + private static readonly System.Type[] FetchResultOperators = + { + typeof (FetchOneRequest), + typeof (FetchManyRequest) + }; + + public SubQueryFromClauseModelVisitor( DataContext dbContext, SqlParameterCollection parameters ) + { + } + + public static void Visit( DataContext dbContext, SqlParameterCollection parameters, QueryModel queryModel ) + { + var subQueryVisitor = new SubQueryFromClauseModelVisitor( dbContext, parameters ); + + subQueryVisitor.VisitQueryModel( queryModel ); + } + + public override void VisitMainFromClause( MainFromClause fromClause, QueryModel queryModel ) + { + if ( fromClause.FromExpression is SubQueryExpression subQueryExpression ) + { + FlattenSubQuery( subQueryExpression, fromClause, queryModel, 0 ); + } + + base.VisitMainFromClause( fromClause, queryModel ); + + } + + public override void VisitAdditionalFromClause( AdditionalFromClause fromClause, QueryModel queryModel, int index ) + { + var subQueryExpression = fromClause.FromExpression as SubQueryExpression; + if ( subQueryExpression != null ) + { + FlattenSubQuery( subQueryExpression, fromClause, queryModel, index ); + } + + base.VisitAdditionalFromClause( fromClause, queryModel, index ); + } + + private static void CopyFromClauseData( FromClauseBase source, FromClauseBase destination ) + { + destination.FromExpression = source.FromExpression; + destination.ItemName = source.ItemName; + destination.ItemType = source.ItemType; + } + + private static void FlattenSubQuery( SubQueryExpression subQueryExpression, FromClauseBase fromClause, QueryModel queryModel, int destinationIndex ) + { + //if ( !CheckFlattenable( subQueryExpression.QueryModel ) ) + // return; + + var mainFromClause = subQueryExpression.QueryModel.MainFromClause; + CopyFromClauseData( mainFromClause, fromClause ); + + var innerSelectorMapping = new QuerySourceMapping(); + innerSelectorMapping.AddMapping( fromClause, subQueryExpression.QueryModel.SelectClause.Selector ); + //queryModel.TransformExpressions( ex => ReferenceReplacingExpressionVisitor.ReplaceClauseReferences( ex, innerSelectorMapping, false ) ); + + InsertBodyClauses( subQueryExpression.QueryModel.BodyClauses, queryModel, destinationIndex ); + InsertResultOperators( subQueryExpression.QueryModel.ResultOperators, queryModel ); + + var innerBodyClauseMapping = new QuerySourceMapping(); + innerBodyClauseMapping.AddMapping( mainFromClause, new QuerySourceReferenceExpression( fromClause ) ); + //queryModel.TransformExpressions( ex => ReferenceReplacingExpressionVisitor.ReplaceClauseReferences( ex, innerBodyClauseMapping, false ) ); + } + + internal static void InsertResultOperators( IEnumerable resultOperators, QueryModel queryModel ) + { + var index = 0; + foreach ( var bodyClause in resultOperators ) + { + queryModel.ResultOperators.Insert( index, bodyClause ); + ++index; + } + } + + private static void InsertBodyClauses( IEnumerable bodyClauses, QueryModel queryModel, int destinationIndex ) + { + foreach ( var bodyClause in bodyClauses ) + { + queryModel.BodyClauses.Insert( destinationIndex, bodyClause ); + ++destinationIndex; + } + } + } +} diff --git a/Achilles.Entities.Sqlite/Modelling/EntityModel.cs b/Achilles.Entities.Sqlite/Modelling/EntityModel.cs index 627c516..0e7bbef 100644 --- a/Achilles.Entities.Sqlite/Modelling/EntityModel.cs +++ b/Achilles.Entities.Sqlite/Modelling/EntityModel.cs @@ -58,6 +58,10 @@ public IEntityMapping GetEntityMapping() where TEntity : class /// public IEntityMapping GetEntityMapping( Type entityType ) => _entityMappings.GetEntityMapping( entityType ); + /// + public bool TryGetEntityMapping( Type entityType, out IEntityMapping entityMapping ) + => _entityMappings.TryGetEntityMapping( entityType, out entityMapping ); + #endregion #region Internal diff --git a/Achilles.Entities.Sqlite/Modelling/IEntityModel.cs b/Achilles.Entities.Sqlite/Modelling/IEntityModel.cs index f429388..9f2eb7b 100644 --- a/Achilles.Entities.Sqlite/Modelling/IEntityModel.cs +++ b/Achilles.Entities.Sqlite/Modelling/IEntityModel.cs @@ -26,6 +26,14 @@ public interface IEntityModel /// A read only collection of . IReadOnlyCollection EntityMappings { get; } + /// + /// + /// + /// + /// + /// + bool TryGetEntityMapping( Type entityType, out IEntityMapping entityMapping ); + /// /// Gets an entity mapping by entity type. /// diff --git a/Achilles.Entities.Sqlite/Modelling/Mapping/EntityMappingCollection.cs b/Achilles.Entities.Sqlite/Modelling/Mapping/EntityMappingCollection.cs index aa67b76..54ac151 100644 --- a/Achilles.Entities.Sqlite/Modelling/Mapping/EntityMappingCollection.cs +++ b/Achilles.Entities.Sqlite/Modelling/Mapping/EntityMappingCollection.cs @@ -60,6 +60,11 @@ public IEntityMapping GetOrAddEntityMapping( Type entityType ) return mapping; } + public bool TryGetEntityMapping( Type entityType, out IEntityMapping entityMapping ) + { + return _entityMappings.TryGetValue( entityType, out entityMapping ); + } + public void TryAddEntityMapping( Type entityType, IEntityMapping entityMapping ) { // TJT: Review this method. @@ -78,6 +83,10 @@ public IEntityMapping GetEntityMapping( Type entityType ) return entityMapping; } + // FIXME: + + // TJT: This seems pretty harsh. Doesn't work with projections in materializer. + throw new ArgumentException( nameof( entityType ) ); } diff --git a/Achilles.Entities.Sqlite/Modelling/Mapping/Materializing/EntityMaterializer.cs b/Achilles.Entities.Sqlite/Modelling/Mapping/Materializing/EntityMaterializer.cs index 829af29..f4a2e69 100644 --- a/Achilles.Entities.Sqlite/Modelling/Mapping/Materializing/EntityMaterializer.cs +++ b/Achilles.Entities.Sqlite/Modelling/Mapping/Materializing/EntityMaterializer.cs @@ -247,33 +247,34 @@ internal object Materialize( IDictionary dictionary, object enti private void SetDeferredLoading ( object entity ) { - // Once we have the entity instance we can attach an entity set source to the instance relationship properties - var entityMapping = _model.GetEntityMapping( entity.GetType() ); - - var relationshipMappings = entityMapping.RelationshipMappings; - - foreach ( var relationshipMapping in relationshipMappings ) + // Projections to DTOs are not in the entity data model. + if ( _model.TryGetEntityMapping( entity.GetType(), out var entityMapping ) ) { - //var foreignKey = relationshipMapping.ForeignKeyMapping; + // Once we have the entity instance we can attach an entity set source to the instance relationship properties. + var relationshipMappings = entityMapping.RelationshipMappings; - if ( relationshipMapping.IsMany ) + foreach ( var relationshipMapping in relationshipMappings ) { - entityMapping.SetEntityCollection( - entity, - relationshipMapping.RelationshipProperty.Name, - relationshipMapping.ForeignKeyMapping ); - } - else - { - entityMapping.SetEntityReference( - entity, - relationshipMapping.RelationshipProperty.Name, - relationshipMapping.ForeignKeyMapping ); - } + //var foreignKey = relationshipMapping.ForeignKeyMapping; - continue; - } + if ( relationshipMapping.IsMany ) + { + entityMapping.SetEntityCollection( + entity, + relationshipMapping.RelationshipProperty.Name, + relationshipMapping.ForeignKeyMapping ); + } + else + { + entityMapping.SetEntityReference( + entity, + relationshipMapping.RelationshipProperty.Name, + relationshipMapping.ForeignKeyMapping ); + } + continue; + } + } } /// diff --git a/Achilles.Entities.Sqlite/Reflection/ReflectionHelpers.cs b/Achilles.Entities.Sqlite/Reflection/ReflectionHelpers.cs index 8b7e778..e77914d 100644 --- a/Achilles.Entities.Sqlite/Reflection/ReflectionHelpers.cs +++ b/Achilles.Entities.Sqlite/Reflection/ReflectionHelpers.cs @@ -10,6 +10,7 @@ #region Namespaces +using Remotion.Linq.Utilities; using System; using System.Linq; using System.Linq.Expressions; @@ -70,5 +71,56 @@ public static MemberInfo GetMemberInfo( LambdaExpression lambda ) } } } + + public static Type GetMemberReturnType( MemberInfo member ) + { + if ( member == null ) + { + throw new ArgumentNullException( nameof( member ) ); + } + + var propertyInfo = member as PropertyInfo; + if ( propertyInfo != null ) + { + return propertyInfo.PropertyType; + } + + var fieldInfo = member as FieldInfo; + if ( fieldInfo != null ) + { + return fieldInfo.FieldType; + } + + var methodInfo = member as MethodInfo; + if ( methodInfo != null ) + { + return methodInfo.ReturnType; + } + + throw new ArgumentException( "Argument must be FieldInfo, PropertyInfo, or MethodInfo.", "member" ); + } + + public static Type GetItemTypeOfClosedGenericIEnumerable( Type enumerableType, string argumentName ) + { + if ( enumerableType == null ) + { + throw new ArgumentNullException( nameof( enumerableType ) ); + } + + if ( string.IsNullOrEmpty( argumentName ) ) + { + throw new ArgumentException( "message", nameof( argumentName ) ); + } + + Type itemType; + if ( !ItemTypeReflectionUtility.TryGetItemTypeOfClosedGenericIEnumerable( enumerableType, out itemType ) ) + { + var message = string.Format( "Expected a closed generic type implementing IEnumerable, but found '{0}'.", enumerableType ); + throw new ArgumentException( message, argumentName ); + } + + return itemType; + } } } + diff --git a/Conduct.md b/Conduct.md new file mode 100644 index 0000000..91ef47a --- /dev/null +++ b/Conduct.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at achilles@telus.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/Contribute.md b/Contribute.md new file mode 100644 index 0000000..7948567 --- /dev/null +++ b/Contribute.md @@ -0,0 +1,33 @@ +## How to contribute to Entities.Sqlite + +#### **Did you find a bug?** + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/achilles-software/entities.sqlite/issues). + +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/achilles-software/entities.sqlite/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. + +#### **Did you write a patch that fixes a bug?** + +* Open a new GitHub pull request with the patch. + +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +* Before submitting, please read the [PR Submissions]](https://github/achilles-software/entities.sqlite/) guide to learn more about coding style and conventions. + +#### **Did you fix whitespace, format code, or make a purely cosmetic patch?** + +Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability will generally not be accepted. + +#### **Do you intend to add a new feature or change an existing one?** + +* Suggest your change by opening a new issue under [Issues](https://github.com/achilles-software/entities.sqlite/issues). + +* Before starting to code collected positive feedback about the change. + +#### **Do you have questions about the source code?** + +* Ask any question about how to use Entities.Sqlite by opening a new issue + +Entities.Sqlite is a volunteer effort. We encourage you to pitch in and join the team! + +Thanks! :heart: :heart: :heart: diff --git a/Entities.Sqlite.Tests/Data/TestDataContext.cs b/Entities.Sqlite.Tests/Data/TestDataContext.cs index 885ca97..7be4cdb 100644 --- a/Entities.Sqlite.Tests/Data/TestDataContext.cs +++ b/Entities.Sqlite.Tests/Data/TestDataContext.cs @@ -122,7 +122,7 @@ public void Initialize() var suppliersList = new List { - new Supplier(){ Name = "Bananas-R-US" }, + new Supplier(){ Name = "Bananas-R-Us" }, new Supplier(){ Name = "Plums-R-Us" } }; diff --git a/Entities.Sqlite.Tests/Querying/DeferredLoadingTest.cs b/Entities.Sqlite.Tests/Querying/DeferredLoadingTest.cs index d65bd36..6902d13 100644 --- a/Entities.Sqlite.Tests/Querying/DeferredLoadingTest.cs +++ b/Entities.Sqlite.Tests/Querying/DeferredLoadingTest.cs @@ -16,7 +16,7 @@ namespace Entities.Sqlite.Tests.Querying public class DeferredLoadingTest { [Fact] - public void EntityReference_LazyLoading_CanLoadEntity() + public void Querying_LazyLoading_CanLoadEntityReference() { const string connectionString = "Data Source=:memory:"; var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options; @@ -34,13 +34,13 @@ public void EntityReference_LazyLoading_CanLoadEntity() var products = query.ToList(); Assert.False( products[ 0 ].Supplier.IsLoaded ); - Assert.Equal( "Bananas-R-US", products[ 0 ].Supplier.Value.Name ); + Assert.Equal( "Bananas-R-Us", products[ 0 ].Supplier.Value.Name ); Assert.True( products[ 0 ].Supplier.IsLoaded ); } } [Fact] - public void EntityCollection_LazyLoading_CanLoadEntityCollection() + public void Querying_LazyLoading_CanLoadEntityCollection() { const string connectionString = "Data Source=:memory:"; var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options; diff --git a/Entities.Sqlite.Tests/Querying/EagerLoadingTest.cs b/Entities.Sqlite.Tests/Querying/EagerLoadingTest.cs index f165769..9db04df 100644 --- a/Entities.Sqlite.Tests/Querying/EagerLoadingTest.cs +++ b/Entities.Sqlite.Tests/Querying/EagerLoadingTest.cs @@ -16,7 +16,7 @@ namespace Entities.Sqlite.Tests.Querying public class EagerLoadingTest { [Fact] - public void EntityReference_EagerLoading_CanLoadEntity() + public void Querying_EagerLoading_CanLoadEntityReference() { const string connectionString = "Data Source=:memory:"; var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options; @@ -25,22 +25,19 @@ public void EntityReference_EagerLoading_CanLoadEntity() { context.Initialize(); - var query = from p in context.Products - join s in context.Suppliers on p.SupplierId equals s.Id into psgroup - from s in psgroup.DefaultIfEmpty() - select new { Product = p, Supplier = s }; + var query = from p in context.Products.FetchOne( p => p.Supplier ) + select p; - var result = query.ToList(); + var products = query.ToList(); - //var result = query.ToList() - // .GroupBy( key => key.Email, element => element.Name ) - // .Select( g => new EmailDto { Subject = g.Key.Subject, Tags = g.Select( t => new TagDto { Name = t } ).ToList() } + Assert.True( products[ 0 ].Supplier.IsLoaded ); + Assert.Equal( "Bananas-R-Us", products[ 0 ].Supplier.Value.Name ); } } [Fact] - public void EntityCollection_EagerLoading_CanLoadEntityCollection() + public void Querying_EagerLoading_CanLoadEntityCollection() { const string connectionString = "Data Source=:memory:"; var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options; @@ -49,41 +46,22 @@ public void EntityCollection_EagerLoading_CanLoadEntityCollection() { context.Initialize(); - var query = from p in context.Products - join s in context.Suppliers on p.SupplierId equals s.Id into ps - from s in ps.DefaultIfEmpty() - select new { Product = p, ProductName = p == null ? "(No products)" : p.Name }; + var query = from p in context.Products.FetchMany( p => p.Parts ) + select p; - var products = query.ToList(); - } - } + var products = query.ToList(); - public class JoinedPost - { - public Product Product { get; set; } - public Supplier Supplier { get; set; } - } - - [Fact] - public void EntityReference_EagerLoading_CanProjectToJoinEntity() - { - const string connectionString = "Data Source=:memory:"; - var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options; + Assert.True( products[ 0 ].Parts.IsLoaded ); - using ( var context = new TestDataContext( options ) ) - { - context.Initialize(); + var parts = products[ 0 ].Parts.ToList(); - var q = from p in context.Products - join s in context.Suppliers on p.Id equals s.Id - select new { Prod = p, Supp = s }; + Assert.Equal( "Bolt", parts[ 0 ].Name ); + Assert.Equal( "Wrench", parts[ 1 ].Name ); + Assert.Equal( "Hammer", parts[ 2 ].Name ); + } - var result = q.ToList(); - //var result = query.ToList() - // .GroupBy( key => key.Email, element => element.Name ) - // .Select( g => new EmailDto { Subject = g.Key.Subject, Tags = g.Select( t => new TagDto { Name = t } ).ToList() } - } } + } } diff --git a/Entities.Sqlite.Tests/Querying/ProjectionTest.cs b/Entities.Sqlite.Tests/Querying/ProjectionTest.cs new file mode 100644 index 0000000..cf372f7 --- /dev/null +++ b/Entities.Sqlite.Tests/Querying/ProjectionTest.cs @@ -0,0 +1,83 @@ +#region Namespaces + +using Achilles.Entities.Linq; +using Achilles.Entities.Configuration; +using Achilles.Entities.Sqlite.Configuration; +using Entities.Sqlite.Tests.Data; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +#endregion + +namespace Entities.Sqlite.Tests.Querying +{ + public class ProjectionsTest + { + public class ProductDto + { + public string ProductDescription { get; set; } + } + + public class JoinedProductSuppliers + { + public Product Product { get; set; } + public Supplier Supplier { get; set; } + } + + [Fact] + public void Queries_Projections_CanProjectToJoinObject() + { + const string connectionString = "Data Source=:memory:"; + var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options; + + using ( var context = new TestDataContext( options ) ) + { + context.Initialize(); + + var q = from p in context.Products + join s in context.Suppliers on p.Id equals s.Id + select new JoinedProductSuppliers + { + Product = p, + Supplier = s + }; + + var result = q.ToList(); + + Assert.Equal( 2, result.Count() ); + + Assert.Equal( "Banana", result[ 0 ].Product.Name ); + Assert.Equal( "Bananas-R-Us", result[ 0 ].Supplier.Name ); + + Assert.Equal( "Plum", result[ 1 ].Product.Name ); + Assert.Equal( "Plums-R-Us", result[ 1 ].Supplier.Name ); + } + } + + [Fact] + public void Queries_Projections_CanProjectToDTO() + { + const string connectionString = "Data Source=:memory:"; + var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options; + + using ( var context = new TestDataContext( options ) ) + { + context.Initialize(); + + var q = from p in context.Products + select new ProductDto + { + ProductDescription = p.Name + }; + + var result = q.ToList(); + + Assert.Equal( 2, result.Count() ); + Assert.Equal( "Banana", result[ 0 ].ProductDescription ); + Assert.Equal( "Plum", result[ 1 ].ProductDescription ); + } + } + } +} diff --git a/Entities.Sqlite.Tests/Querying/QueriesTest.cs b/Entities.Sqlite.Tests/Querying/QueriesTest.cs index 39b1050..cf7399e 100644 --- a/Entities.Sqlite.Tests/Querying/QueriesTest.cs +++ b/Entities.Sqlite.Tests/Querying/QueriesTest.cs @@ -16,7 +16,7 @@ namespace Entities.Sqlite.Tests.Querying public class QueriesTest { [Fact] - public void LQueries_ComplexQuery_CanReadList() + public void Queries_Simple_CanReadList() { const string connectionString = "Data Source=:memory:"; var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options; @@ -39,7 +39,7 @@ public void LQueries_ComplexQuery_CanReadList() } [Fact] - public async Task Querying__ComplexQuery_CanReadListAsync() + public async Task Querying_Simple_CanReadListAsync() { const string connectionString = "Data Source=:memory:"; var options = new DataContextOptionsBuilder().UseSqlite( connectionString ).Options;