1 //---------------------------------------------------------------------
2 // <copyright file="ObjectQuery_EntitySqlExtensions.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
7 // @backupowner Microsoft
8 //---------------------------------------------------------------------
10 namespace System.Data.Objects
13 using System.Collections.Generic;
15 using System.Data.Common;
16 using System.Data.Common.CommandTrees;
17 using System.Data.Common.CommandTrees.ExpressionBuilder;
18 using System.Data.Common.Utils;
19 using System.Data.Metadata.Edm;
20 using System.Data.Objects.ELinq;
21 using System.Data.Objects.Internal;
22 using System.Globalization;
26 /// ObjectQuery implements strongly-typed queries at the object-layer.
27 /// Queries are specified using Entity-SQL strings and may be created by calling
28 /// the Entity-SQL-based query builder methods declared by ObjectQuery.
30 /// <typeparam name="T">The result type of this ObjectQuery</typeparam>
31 public partial class ObjectQuery<T> : IEnumerable<T>
33 #region Private Static Members
40 /// The default query name, which is used in query-building to refer to an
41 /// element of the ObjectQuery; e.g., in a call to ObjectQuery.Where(), a predicate of
42 /// the form "it.Name = 'Foo'" can be specified, where "it" refers to a T.
43 /// Note that the query name may eventually become a parameter in the command
44 /// tree, so it must conform to the parameter name restrictions enforced by
45 /// ObjectParameter.ValidateParameterName(string).
47 private const string DefaultName = "it";
49 private static bool IsLinqQuery(ObjectQuery query)
51 return (query.QueryState is ELinqQueryState);
56 #region Private Instance Members
58 // -------------------
60 // -------------------
63 /// The name of the current sequence, which defaults to "it". Used in query-
64 /// builder methods that process an Entity-SQL command text fragment to refer to an
65 /// instance of the return type of this query.
67 private string _name = ObjectQuery<T>.DefaultName;
71 #region Public Constructors
73 // -------------------
74 // Public Constructors
75 // -------------------
77 #region ObjectQuery (string, ObjectContext)
80 /// This constructor creates a new ObjectQuery instance using the specified Entity-SQL
81 /// command as the initial query. The context specifies the connection on
82 /// which to execute the query as well as the metadata and result cache.
84 /// <param name="commandText">
85 /// The Entity-SQL query string that initially defines the query.
87 /// <param name="context">
88 /// The ObjectContext containing the metadata workspace the query will
89 /// be built against, the connection on which to execute the query, and the
90 /// cache to store the results in.
93 /// A new ObjectQuery instance.
95 public ObjectQuery (string commandText, ObjectContext context)
96 : this(new EntitySqlQueryState(typeof(T), commandText, false, context, null, null))
98 // SQLBUDT 447285: Ensure the assembly containing the entity's CLR type
99 // is loaded into the workspace. If the schema types are not loaded
100 // metadata, cache & query would be unable to reason about the type. We
101 // either auto-load <T>'s assembly into the ObjectItemCollection or we
102 // auto-load the user's calling assembly and its referenced assemblies.
103 // If the entities in the user's result spans multiple assemblies, the
104 // user must manually call LoadFromAssembly. *GetCallingAssembly returns
105 // the assembly of the method that invoked the currently executing method.
106 context.MetadataWorkspace.ImplicitLoadAssemblyForType(typeof(T), System.Reflection.Assembly.GetCallingAssembly());
111 #region ObjectQuery (string, ObjectContext, MergeOption)
114 /// This constructor creates a new ObjectQuery instance using the specified Entity-SQL
115 /// command as the initial query. The context specifies the connection on
116 /// which to execute the query as well as the metadata and result cache.
117 /// The merge option specifies how the cache should be populated/updated.
119 /// <param name="commandText">
120 /// The Entity-SQL query string that initially defines the query.
122 /// <param name="context">
123 /// The ObjectContext containing the metadata workspace the query will
124 /// be built against, the connection on which to execute the query, and the
125 /// cache to store the results in.
127 /// <param name="mergeOption">
128 /// The MergeOption to use when executing the query.
131 /// A new ObjectQuery instance.
133 public ObjectQuery (string commandText, ObjectContext context, MergeOption mergeOption)
134 : this(new EntitySqlQueryState(typeof(T), commandText, false, context, null, null))
136 EntityUtil.CheckArgumentMergeOption(mergeOption);
137 this.QueryState.UserSpecifiedMergeOption = mergeOption;
139 // SQLBUDT 447285: Ensure the assembly containing the entity's CLR type
140 // is loaded into the workspace. If the schema types are not loaded
141 // metadata, cache & query would be unable to reason about the type. We
142 // either auto-load <T>'s assembly into the ObjectItemCollection or we
143 // auto-load the user's calling assembly and its referenced assemblies.
144 // If the entities in the user's result spans multiple assemblies, the
145 // user must manually call LoadFromAssembly. *GetCallingAssembly returns
146 // the assembly of the method that invoked the currently executing method.
147 context.MetadataWorkspace.ImplicitLoadAssemblyForType(typeof(T), System.Reflection.Assembly.GetCallingAssembly());
154 #region internal ObjectQuery (EntitySet, ObjectContext, MergeOption) constructor.
157 /// This method creates a new ObjectQuery instance that represents a scan over
158 /// the specified <paramref name="entitySet"/>. This ObjectQuery carries the scan as <see cref="DbExpression"/>
159 /// and as Entity SQL. This is needed to allow case-sensitive metadata access (provided by the <see cref="DbExpression"/> by default).
160 /// The context specifies the connection on which to execute the query as well as the metadata and result cache.
161 /// The merge option specifies how the cache should be populated/updated.
163 /// <param name="entitySet">
164 /// The entity set this query scans.
166 /// <param name="context">
167 /// The ObjectContext containing the metadata workspace the query will
168 /// be built against, the connection on which to execute the query, and the
169 /// cache to store the results in.
171 /// <param name="mergeOption">
172 /// The MergeOption to use when executing the query.
175 /// A new ObjectQuery instance.
177 internal ObjectQuery (EntitySetBase entitySet, ObjectContext context, MergeOption mergeOption)
178 : this(new EntitySqlQueryState(typeof(T), BuildScanEntitySetEsql(entitySet), entitySet.Scan(), false, context, null, null))
180 EntityUtil.CheckArgumentMergeOption(mergeOption);
181 this.QueryState.UserSpecifiedMergeOption = mergeOption;
183 // SQLBUDT 447285: Ensure the assembly containing the entity's CLR type
184 // is loaded into the workspace. If the schema types are not loaded
185 // metadata, cache & query would be unable to reason about the type. We
186 // either auto-load <T>'s assembly into the ObjectItemCollection or we
187 // auto-load the user's calling assembly and its referenced assemblies.
188 // If the entities in the user's result spans multiple assemblies, the
189 // user must manually call LoadFromAssembly. *GetCallingAssembly returns
190 // the assembly of the method that invoked the currently executing method.
191 context.MetadataWorkspace.ImplicitLoadAssemblyForType(typeof(T), System.Reflection.Assembly.GetCallingAssembly());
194 private static string BuildScanEntitySetEsql(EntitySetBase entitySet)
196 EntityUtil.CheckArgumentNull(entitySet, "entitySet");
197 return String.Format(
198 CultureInfo.InvariantCulture,
200 EntityUtil.QuoteIdentifier(entitySet.EntityContainer.Name),
201 EntityUtil.QuoteIdentifier(entitySet.Name));
206 #region Public Properties
209 /// The name of the query, which can be used to identify the current sequence
210 /// by name in query-builder methods. By default, the value is "it".
212 /// <exception cref="ArgumentException">
213 /// If the value specified on set is invalid.
223 EntityUtil.CheckArgumentNull(value, "value");
225 if (!ObjectParameter.ValidateParameterName(value))
227 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_InvalidQueryName(value), "value");
236 #region Query-builder Methods
238 // ---------------------
239 // Query-builder Methods
240 // ---------------------
243 /// This query-builder method creates a new query whose results are the
244 /// unique results of this query.
247 /// a new ObjectQuery instance.
249 public ObjectQuery<T> Distinct ()
251 if (IsLinqQuery(this))
253 return (ObjectQuery<T>)Queryable.Distinct<T>(this);
255 return new ObjectQuery<T>(EntitySqlQueryBuilder.Distinct(this.QueryState));
259 /// This query-builder method creates a new query whose results are all of
260 /// the results of this query, except those that are also part of the other
263 /// <param name="query">
264 /// A query representing the results to exclude.
267 /// a new ObjectQuery instance.
269 /// <exception cref="ArgumentNullException">
270 /// If the query parameter is null.
272 public ObjectQuery<T> Except(ObjectQuery<T> query)
274 EntityUtil.CheckArgumentNull(query, "query");
276 if (IsLinqQuery(this) || IsLinqQuery(query))
278 return (ObjectQuery<T>)Queryable.Except(this, query);
280 return new ObjectQuery<T>(EntitySqlQueryBuilder.Except(this.QueryState, query.QueryState));
284 /// This query-builder method creates a new query whose results are the results
285 /// of this query, grouped by some criteria.
287 /// <param name="keys">
290 /// <param name="projection">
291 /// The projection list. To project the group, use the keyword "group".
293 /// <param name="parameters">
294 /// An optional set of query parameters that should be in scope when parsing.
297 /// a new ObjectQuery instance.
299 public ObjectQuery<DbDataRecord> GroupBy(string keys, string projection, params ObjectParameter[] parameters)
301 EntityUtil.CheckArgumentNull(keys, "keys");
302 EntityUtil.CheckArgumentNull(projection, "projection");
303 EntityUtil.CheckArgumentNull(parameters, "parameters");
305 if (StringUtil.IsNullOrEmptyOrWhiteSpace(keys))
307 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidGroupKeyList, "keys");
310 if (StringUtil.IsNullOrEmptyOrWhiteSpace(projection))
312 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidProjectionList, "projection");
315 return new ObjectQuery<DbDataRecord>(EntitySqlQueryBuilder.GroupBy(this.QueryState, this.Name, keys, projection, parameters));
319 /// This query-builder method creates a new query whose results are those that
320 /// are both in this query and the other query specified.
322 /// <param name="query">
323 /// A query representing the results to intersect with.
326 /// a new ObjectQuery instance.
328 /// <exception cref="ArgumentNullException">
329 /// If the query parameter is null.
331 public ObjectQuery<T> Intersect (ObjectQuery<T> query)
333 EntityUtil.CheckArgumentNull(query, "query");
335 if (IsLinqQuery(this) || IsLinqQuery(query))
337 return (ObjectQuery<T>)Queryable.Intersect(this, query);
339 return new ObjectQuery<T>(EntitySqlQueryBuilder.Intersect(this.QueryState, query.QueryState));
343 /// This query-builder method creates a new query whose results are filtered
344 /// to include only those of the specified type.
347 /// a new ObjectQuery instance.
349 /// <exception cref="EntitySqlException">
350 /// If the type specified is invalid.
352 public ObjectQuery<TResultType> OfType<TResultType>()
354 if (IsLinqQuery(this))
356 return (ObjectQuery<TResultType>)Queryable.OfType<TResultType>(this);
359 // SQLPUDT 484477: Make sure TResultType is loaded.
360 this.QueryState.ObjectContext.MetadataWorkspace.ImplicitLoadAssemblyForType(typeof(TResultType), System.Reflection.Assembly.GetCallingAssembly());
362 // Retrieve the O-Space type metadata for the result type specified. If no
363 // metadata can be found for the specified type, fail. Otherwise, if the
364 // type metadata found for TResultType is not either an EntityType or a
365 // ComplexType, fail - OfType() is not a valid operation on scalars,
366 // enumerations, collections, etc.
367 Type clrOfType = typeof(TResultType);
368 EdmType ofType = null;
369 if (!this.QueryState.ObjectContext.MetadataWorkspace.GetItemCollection(DataSpace.OSpace).TryGetType(clrOfType.Name, clrOfType.Namespace ?? string.Empty, out ofType) ||
370 !(Helper.IsEntityType(ofType) || Helper.IsComplexType(ofType)))
372 throw EntityUtil.EntitySqlError(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidResultType(typeof(TResultType).FullName));
375 return new ObjectQuery<TResultType>(EntitySqlQueryBuilder.OfType(this.QueryState, ofType, clrOfType));
379 /// This query-builder method creates a new query whose results are the
380 /// results of this query, ordered by some criteria. Note that any relational
381 /// operations performed after an OrderBy have the potential to "undo" the
382 /// ordering, so OrderBy should be considered a terminal query-building
385 /// <param name="keys">
388 /// <param name="parameters">
389 /// An optional set of query parameters that should be in scope when parsing.
392 /// a new ObjectQuery instance.
394 /// <exception cref="ArgumentNullException">
395 /// If either argument is null.
397 /// <exception cref="ArgumentException">
398 /// If the sort key command text is empty.
400 public ObjectQuery<T> OrderBy (string keys, params ObjectParameter[] parameters)
402 EntityUtil.CheckArgumentNull(keys, "keys");
403 EntityUtil.CheckArgumentNull(parameters, "parameters");
405 if (StringUtil.IsNullOrEmptyOrWhiteSpace(keys))
407 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidSortKeyList, "keys");
410 return new ObjectQuery<T>(EntitySqlQueryBuilder.OrderBy(this.QueryState, this.Name, keys, parameters));
414 /// This query-builder method creates a new query whose results are data
415 /// records containing selected fields of the results of this query.
417 /// <param name="projection">
418 /// The projection list.
420 /// <param name="parameters">
421 /// An optional set of query parameters that should be in scope when parsing.
424 /// a new ObjectQuery instance.
426 /// <exception cref="ArgumentNullException">
427 /// If either argument is null.
429 /// <exception cref="ArgumentException">
430 /// If the projection list command text is empty.
432 public ObjectQuery<DbDataRecord> Select (string projection, params ObjectParameter[] parameters)
434 EntityUtil.CheckArgumentNull(projection, "projection");
435 EntityUtil.CheckArgumentNull(parameters, "parameters");
437 if (StringUtil.IsNullOrEmptyOrWhiteSpace(projection))
439 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidProjectionList, "projection");
442 return new ObjectQuery<DbDataRecord>(EntitySqlQueryBuilder.Select(this.QueryState, this.Name, projection, parameters));
446 /// This query-builder method creates a new query whose results are a sequence
447 /// of values projected from the results of this query.
449 /// <param name="projection">
450 /// The projection list.
452 /// <param name="parameters">
453 /// An optional set of query parameters that should be in scope when parsing.
456 /// a new ObjectQuery instance.
458 /// <exception cref="ArgumentNullException">
459 /// If either argument is null.
461 /// <exception cref="ArgumentException">
462 /// If the projection list command text is empty.
464 public ObjectQuery<TResultType> SelectValue<TResultType> (string projection, params ObjectParameter[] parameters)
466 EntityUtil.CheckArgumentNull(projection, "projection");
467 EntityUtil.CheckArgumentNull(parameters, "parameters");
469 if (StringUtil.IsNullOrEmptyOrWhiteSpace(projection))
471 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidProjectionList, "projection");
474 // SQLPUDT 484974: Make sure TResultType is loaded.
475 this.QueryState.ObjectContext.MetadataWorkspace.ImplicitLoadAssemblyForType(typeof(TResultType), System.Reflection.Assembly.GetCallingAssembly());
477 return new ObjectQuery<TResultType>(EntitySqlQueryBuilder.SelectValue(this.QueryState, this.Name, projection, parameters, typeof(TResultType)));
481 /// This query-builder method creates a new query whose results are the
482 /// results of this query, ordered by some criteria and with the specified
483 /// number of results 'skipped', or paged-over.
485 /// <param name="keys">
488 /// <param name="count">
489 /// Specifies the number of results to skip. This must be either a constant or
490 /// a parameter reference.
492 /// <param name="parameters">
493 /// An optional set of query parameters that should be in scope when parsing.
496 /// a new ObjectQuery instance.
498 /// <exception cref="ArgumentNullException">
499 /// If any argument is null.
501 /// <exception cref="ArgumentException">
502 /// If the sort key or skip count command text is empty.
504 public ObjectQuery<T> Skip (string keys, string count, params ObjectParameter[] parameters)
506 EntityUtil.CheckArgumentNull(keys, "keys");
507 EntityUtil.CheckArgumentNull(count, "count");
508 EntityUtil.CheckArgumentNull(parameters, "parameters");
510 if (StringUtil.IsNullOrEmptyOrWhiteSpace(keys))
512 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidSortKeyList, "keys");
515 if (StringUtil.IsNullOrEmptyOrWhiteSpace(count))
517 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidSkipCount, "count");
520 return new ObjectQuery<T>(EntitySqlQueryBuilder.Skip(this.QueryState, this.Name, keys, count, parameters));
524 /// This query-builder method creates a new query whose results are the
525 /// first 'count' results of this query.
527 /// <param name="count">
528 /// Specifies the number of results to return. This must be either a constant or
529 /// a parameter reference.
531 /// <param name="parameters">
532 /// An optional set of query parameters that should be in scope when parsing.
535 /// a new ObjectQuery instance.
537 /// <exception cref="ArgumentNullException">
538 /// If the top count command text is null.
540 /// <exception cref="ArgumentException">
541 /// If the top count command text is empty.
543 public ObjectQuery<T> Top (string count, params ObjectParameter[] parameters)
545 EntityUtil.CheckArgumentNull(count, "count");
547 if (StringUtil.IsNullOrEmptyOrWhiteSpace(count))
549 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidTopCount, "count");
552 return new ObjectQuery<T>(EntitySqlQueryBuilder.Top(this.QueryState, this.Name, count, parameters));
556 /// This query-builder method creates a new query whose results are all of
557 /// the results of this query, plus all of the results of the other query,
558 /// without duplicates (i.e., results are unique).
560 /// <param name="query">
561 /// A query representing the results to add.
564 /// a new ObjectQuery instance.
566 /// <exception cref="ArgumentNullException">
567 /// If the query parameter is null.
569 public ObjectQuery<T> Union (ObjectQuery<T> query)
571 EntityUtil.CheckArgumentNull(query, "query");
573 if (IsLinqQuery(this) || IsLinqQuery(query))
575 return (ObjectQuery<T>)Queryable.Union(this, query);
577 return new ObjectQuery<T>(EntitySqlQueryBuilder.Union(this.QueryState, query.QueryState));
581 /// This query-builder method creates a new query whose results are all of
582 /// the results of this query, plus all of the results of the other query,
583 /// including any duplicates (i.e., results are not necessarily unique).
585 /// <param name="query">
586 /// A query representing the results to add.
589 /// a new ObjectQuery instance.
591 /// <exception cref="ArgumentNullException">
592 /// If the query parameter is null.
594 public ObjectQuery<T> UnionAll (ObjectQuery<T> query)
596 EntityUtil.CheckArgumentNull(query, "query");
598 return new ObjectQuery<T>(EntitySqlQueryBuilder.UnionAll(this.QueryState, query.QueryState));
602 /// This query-builder method creates a new query whose results are the
603 /// results of this query filtered by some criteria.
605 /// <param name="predicate">
606 /// The filter predicate.
608 /// <param name="parameters">
609 /// An optional set of query parameters that should be in scope when parsing.
612 /// a new ObjectQuery instance.
614 /// <exception cref="ArgumentNullException">
615 /// If either argument is null.
617 /// <exception cref="ArgumentException">
618 /// If the filter predicate command text is empty.
620 public ObjectQuery<T> Where (string predicate, params ObjectParameter[] parameters)
622 EntityUtil.CheckArgumentNull(predicate, "predicate");
623 EntityUtil.CheckArgumentNull(parameters, "parameters");
625 if (StringUtil.IsNullOrEmptyOrWhiteSpace(predicate))
627 throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidFilterPredicate, "predicate");
630 return new ObjectQuery<T>(EntitySqlQueryBuilder.Where(this.QueryState, this.Name, predicate, parameters));