Queries (Specification Pattern)
- Queries are built on the extended specification pattern.
- Use ISpecification<TEntity> or ISpecificationWithParams<TEntity> (for final queries) as a return type instead of D3Specification<TEntity> or D3SpecificationWithParams<TEntity>.
- For better performance and less architecture impact, do not forget to keep prebuilt queries unchanged and static as possible.
- QueryContainer will help you with that.
- Beware of correct placement of parentheses not to get unwanted joins or filtering in query strings. see more...
Important Note: If a specification is not prebuilt and you join tables or filter or sort data into it, then the clone (copy) of original specification is made to keep the original query unchanged. But it is done only for the first time, if you want to reuse the new specification clone, call specification.Prebuild()
Query Building (Join)
- Start with D3Specification.Create<TEntity>(D3Context, forceInnerJoin = false, forceLeftJoin = false) to create single table query (SELECT). see more...
- Join tables by And(ISpecification) or by Or(ISpecification) where ISpecification is a base type for ISpecification<TEntity> where TEntity is any generic type limited to class and new().
- If the parent type has 2 or more references to the joining (child) type and you do not want to include all the references, you can also use the overload of D3Specification.Create<TEntity>(ID3Context, bool, bool)
// Let's assume that we have Match with 2 references to Team - HomeTeam and AwayTeam and we want to include both references.
D3Specification.Create<Match>(D3Context).And(D3Specification.Create<Team>(D3Context)); - If you want to include a specific property only, use the overload with lambda expression where you specify propToIncludeOnly - the only reference that is related to the property is included.
// Let's assume that we have Match with 2 references to Team - HomeTeam and AwayTeam and we want to include HomeTeam only.
D3Specification.Create<Match>(D3Context).And(D3Specification.Create(D3Context, (Match x) => x.HomeTeam)); - Beware of joining a table which is not accessible from the original query (already joined) tables - an error will be thrown.
- This can also happen to you if you do not place parentheses correctly.
- The third overload Create<TInverseRoot, TResult>(..) is used if you want to build a query from a collection side, but you want to get some item's relation as a root result. It is useable if you want to sort by a collection. sorting by a collection
Query Building (Filter)
- Use CompareFormat(..) function to create a placeholder of a comparison for a property. The placeholder is substituted by a parameter constant when building final SQL.
- If you call D3Specification.BuildSQL(spec, bool, bool, parameters) and you attach no parameter for a specific integer key, the comparison is omitted.
- Use Convert.DBNull to compare a property with NULL.
- Use Or() when you specified if the child query contains a filter that you want to apply for reference 1 or reference 2.
- Use Compare(..) to set the filter argument value for a the property directly. see more...
- Parameter ComparisonOp op: Specifies the comparison-like operator of the comparison.
- Parameter LogicalOp appendWithOp: Specifies which operator will be used when appending the comparison to an existing (original) filter.
- i.e.: Use LogicalOp.AND | LogicalOp.DuplicitOr If you join a child table that is referenced twice or more times without usage of propToIncludeOnly. The filter will be appended as AND (prop1 .. OR prop2 .. )
!!! IMPORTANT !!! Nesting filters using And() and Or() functions takes precedence over appendWithOp as they usually cover a whole expression by parentheses if the (original) current "nested" level filter's logical operators differ from the operator you add by And() or Or() functions.
First filter item of the expression is appended with no logical operator.
//
// Example 1: Team.Name = 'Team1'
//
D3SQLAdapter Example1()
{
var spec = D3Specification.Create<Team>(D3Context).Compare(x => x.Name, ComparisonOp.EQUALS, "Team1");
return D3Specification.BuildSQL(spec, useSkipAndTakeFormat, debugMode, parameters); // auto PreBuild() when calling BuildSQL(..) on not built specification.
}
//
// Example 2: ( ( Match.HomeTeam.Name = {0} OR Match.AwayTeam.Name = {0} ) OR Match.MatchDate >= '{1}' ) AND Match.TournamentPhase = {2}
//
ISpecification<Match> Example2()
{
// Team.name = '{0}'
var teamSpec = D3Specification.Create<Team>(D3Context).CompareFormat(x => x.Name, ComparisonOp.EQUALS, 0);
// Match.HomeTeam.Name = {0} OR Match.AwayTeam.Name = {0} ... Match has 2 references from Team and no `propToIncludeOnly` is specified -> so, Team1 could be a home or an away team.
var spec = D3Specification.Create<Match>(D3Context).Or(teamSpec);
// ( Match.HomeTeam.Name = {0} OR Match.AwayTeam.Name = {0} ) OR Match.MatchDate >= '{1}'
spec = spec.CompareFormat(x => x.MatchDate, ComparisonOp.GREATER_THAN_OR_EQUAL, 1, appendWithOp: LogicalOp.OR);
// ^^Nesting of the original filter has been made even if And() or Or() has not been used, because 2 reference relations are always nested.
if (spec is D3BaseSpecification d3Spec)
{
var clone = d3Spec.CloneUnsafe() as D3Specification<Match>; // spec must be kept unchanged
// Wrong Filter Nesting: ( Match.HomeTeam.Name = {0} OR Match.AwayTeam.Name = {0} ) OR Match.MatchDate >= '{1}' AND Match.TournamentPhase = {2}
var toDispose = clone?.CompareFormat(x => x.TournamentPhase, ComparisonOp.EQUALS, 2/*, appendWithOp: LogicalOp.AND not necessary*/);
//^^ This construction will not nest the left side (original) filter expression into parentheses expression - you will have to use And() or Or().
}
// Correct Filter Nesting: ( ( Match.HomeTeam.Name = {0} OR Match.AwayTeam.Name = {0} ) OR Match.MatchDate >= '{1}' ) AND Match.TournamentPhase = {2}
spec = spec.And(D3Specification.Create<Match>(D3Context).CompareFormat(x => x.TournamentPhase, ComparisonOp.EQUALS, 2)/*, appendWithOp: LogicalOp.AND not necessary*/);
//^^ Nesting Filters: Each time, when a new specification is appended by And() or Or() with a different logical operator than the top original filter context consists of, then the top context or also the new expression (if contains any logical operator) is covered by parentheses.
D3Specification.PreBuild(spec);
return spec;
}
//
// Example 2: Finalize query
//
ISpecificationWithParams<Match> Example2WithParams(string teamName, DateTime matchFromDate)
{
return new D3SpecificationWithParams<Match>(Example2())
{
Parameters = new Dictionary<int, object?>
{
{ 0, teamName },
{ 1, matchFromDate }
}
};
}
- Use Not() to negate top filter context.
- Use In() or NotIn() to create "IN" or "NOT IN" filter expression on id column. see more...
Query Building (Sort)
- Use OrderBy() function to specify which column to sort by.
- Parameter uint sortOrder: Specifies the order of the column to sort. The value for the first column order is 1.
- If you specify OrderBy(..) for a same property, then it is overridden if the original sortOrder is higher (worse position) or replaceOldSortOrder is true.
- If you do not specify sortOrder then the offset of 10 + number of sort columns already used is set to evaluate the sortOrder.
- Do not use collection properties before its parent properties, if you do so, parents' id properties will be placed before collection properties due to performance (Entity Framework does the same). To sort from a collection side use "inverse query" sorting by a collection.
spec = spec.OrderBy(x => x.MatchDate, desc: true) // sortOrder 10
.OrderBy(x => x.TournamentId, sortOrder: 1) // sortOrder 1
.OrderBy(x => x.HomeTeam, (Team x) => x.Name) // sortOrder 12
.And(D3Specification.Create<Team>().OrderBy(x => x.Name)); // HomeTeam sortOrder already specified, AwayTeam sortOrder 13
Query Building (DTOs)
- D3ORM supports returning DTO query results directly from the database - just use ISpecification.AsDTO(..) on the top entity and optionally on the other joined entities if you need to specify specific name exceptions.
- Example of Complex Filter and Complex Join Query
D3Specification.Create<Match>(D3Context)
.AsDTO(new Tuple<Expression<Func<Match, object?>>, Expression<Func<MatchTestDTO, object?>>>(x => x.WinnerId, y => y.WinnerIdTest))
.And(QC.TournamentBaseQuery
.In(QC.TournamentBaseQuery
.NotIn(y => y.TournamentId, new int[] { 3, 4 }, LogicalOp.AND)
.And(QC.TeamIdFilterQuery(null).Compare(y => y.RegistrationDate, ComparisonOp.GREATER_THAN_OR_EQUAL, new DateTime(2021, 1, 1)))))
.And(QC.TeamBaseQuery
.And(D3Specification.Create<User>(D3Context)
.AsDTO<UserDTO>())
.Or(D3Specification.Create<TournamentTeam>(D3Context).Compare(x => x.TournamentTeamId, ComparisonOp.EQUALS, 1).Compare(x => x.TournamentTeamId, ComparisonOp.EQUALS, 2, LogicalOp.OR)))
.Or(QC.TeamEmptyQuery.Compare(x => x.Name, ComparisonOp.LIKE, "noname%").OrderBy(x => x.Name, true, 1)
.And(D3Specification.Create<TournamentTeam>(D3Context))
.And(D3Specification.Create<Tournament>(D3Context).Compare(x => x.Name, ComparisonOp.LIKE, "noname%", LogicalOp.AND)))
.And(QC.TournamentBaseQuery.OrderBy(x => x.Name, false, 2))
.AsDTO<PlayoffRoundCoupleDTO>()
Building DTO Queries Benefits:
- Building DTO lists directly from the query: Less server side conversions between entities and DTOs.
- Omitting and renaming of the property names allowed.
Building DTO Queries Constraints:
- All DTOs of a single instance (with aggregates) must be placed into the same namespace or defined in some namespace from ID3Context.OtherModelNamespaces.
- If any lower part of the specification is annotated with AsDTO, the top specification part should be too.
- DTOs do not have to remain all properties, but must remain id properties - you can use JsonIgnoreAttribute not to include them in a HTTP response.
- DTO names must contain its entity name.
- Id property names must remain the same as the entity name or at least should follow the id property pattern with a DTO name containing the original entity name instead of the entity name itself (the same for navigation properties).
- Use original entity name, or entity name with a suffix defined by ID3Context.DtoPatternFormat.
- i.e.: PlayoffRoundDTO, MatchDTO... or i.e.: PlayoffRoundDTO, Match; i.e.: TournamentTeamRank, TournamentTeamStatRank or i.e.: TournamentTeamRank, TournamentTeamStatDTO..
- The namespace and DTO name constraints do not have to applied for DTOs created at the server side (after fetching all the data from a database).
Query Building (PreBuild)
- To reuse a single instance of a specification clone it or prebuild it.
- After you start building a specification it is clonned since the second building method is called, but also the returned result can be used only once without CloneUnsafe(..) or PreBuild(..)
var teamSpec = D3Specification.Create<Team>(D3Context).And(teamPlayer2Spec).CompareFormat(x => x.Name, ComparisonOp.EQUALS, 0)/*.CompareFormat(...And( etc whatever*/;
// Without this call:
// 1. The teamSpec2 and also the teamSpec3 would be the same instances as teamSpec.
// 2. All the teamSpecX pointers would have defined all the filters from teamSpec and also from teamPlayer2Spec and teamPlayer3Spec as they point to the same instance.
D3Specification.PreBuild(teamSpec);
var teamSpec2 = teamSpec.And(teamPlayer2Spec);
var teamSpec3 = teamSpec.And(teamPlayer3Spec);
Query Building (Special Cases)
Count Rows
- Use CountRows() to create a "count rows" query SELECT count(id)...
ISpecification<TEntity> CountRows();
Skip and Take
// To skip first 200 rows and include rows from 200 to 299 use:
var sqlAdapter = D3Specification.BuildSQL(spec, useSkipAndTakeFormat: true, debugMode: false, parameters: spec.Parameters);
DbConnection.AggSelect<TEntity>(D3Context, sqlAdapter, skip: 200, take: 100);
AVG, COUNT, MIN, MAX, SUM
- To build computing aggregation function use one of theese:
- To do more complex operation across more columns use CustomAggFunc(..) or CustomChildAggFunc(..) and optionally apply AVG, COUNT, MIN, MAX or SUM. see an example...
ISpecification<TEntity> Avg(int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties);
ISpecification<TEntity> Avg<TParent, TChild>(Expression<Func<TParent, TChild>> parentProp, int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties) where TParent : class where TChild : class;
ISpecification<TEntity> Count(int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties);
ISpecification<TEntity> Count<TParent, TChild>(Expression<Func<TParent, TChild>> parentProp, int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties) where TParent : class where TChild : class;
ISpecification<TEntity> Min(int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties);
ISpecification<TEntity> Min<TParent, TChild>(Expression<Func<TParent, TChild>> parentProp, int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties) where TParent : class where TChild : class;
ISpecification<TEntity> Max(int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties);
ISpecification<TEntity> Max<TParent, TChild>(Expression<Func<TParent, TChild>> parentProp, int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties) where TParent : class where TChild : class;
ISpecification<TEntity> Sum(int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties);
ISpecification<TEntity> Sum<TParent, TChild>(Expression<Func<TParent, TChild>> parentProp, int? propertySortOrder = null, bool includeAllByDefault = false, params Expression<Func<TEntity, IComparable?>>[] properties) where TParent : class where TChild : class;
Inverse Query (Sorting by a Collection)
- If you want to sort by a collection you have to build the query from a collection side and define so called "inverse root" query, otherwise all root (parent) ids will be placed before the first collection property in the sort clause.
- Let's say we want to sort TournamentTeams by its statistics from a specific tournament phase and we have more statistics for each TournamentTeam (one for each phase).
- The result is a TournamentTeam list sorted by TournamentTeamStats.
- This can be used to make play-off couples.
D3Specification.Create(d3Context, (TournamentTeam x) => x.TournamentTeamStats, (TournamentTeamStat x) => x.TournamentTeamId)
.And(D3Specification.Create<TournamentTeamStat>(d3Context)
.OrderBy(x => x.PhasePoints, true, 3)
.CustomAggFunc(nameof(TournamentTeamStatRank.SetsWonLostDiff), ComputeType.NONE, x => x.SetsWon, CrossColumnOp.DiffOrNone, x => x.SetsLost, propertySortOrder: 4)
.CustomAggFunc(nameof(TournamentTeamStatRank.ScorePlusMinus), ComputeType.NONE, x => x.ScorePlus, CrossColumnOp.DiffOrNone, x => x.ScoreMinus, propertySortOrder: 5)
.CompareFormat(x => x.TournamentPhase, ComparisonOp.EQUALS, 1)
.OrderBy(x => x.TournamentPhase, false, 1))
.And(D3Specification.Create<TournamentTeam>(d3Context)
.CompareFormat(x => x.TournamentId, ComparisonOp.EQUALS, 0)
.And(D3Specification.Create<Team>(d3Context))
.OrderBy(x => x.BasicGroupName, false, 2))
.IncludeSelectProps((TournamentTeamStat x) => x.PhasePoints, (TournamentTeamStat x) => x.TournamentPhase, (TournamentTeamStat x) => x.LastChange)
.IncludeSelectProps((TournamentTeam x) => x.BasicGroupName, (TournamentTeam x) => x.LastChange)
.IncludeSelectProps((Team x) => "*")
.AsDTO<TournamentTeamRank>();
Execute Query
- As you have all queries static and prebuilt using QueryContainer use the an extension from Data Reading Extensions to read the data from a database.
using (var connection = CreateConnection(connectionString))
{
var finalSpec = QC.MatchBaseFilterByTeamNameQuery("Team1", new DateTime(2022, 9, 5));
return connection.AggSelect<Match>(D3Context, finalSpec.BuildSQL(finalSpec, false, false, finalSpec.Parameters));
// ...or use 'skip and take' query
return connection.AggSelect<Match>(D3Context, finalSpec.BuildSQL(finalSpec, true, false, finalSpec.Parameters), 0, 100);
}
// or
using (var connection = CreateConnection(connectionString))
{
var computeSpec = ...;
return connection.AggCompute<ComputeSpecDTO/*or object*/>(D3Context, computeSpec.BuildSQL(finalSpec, false, false, computeSpec.Parameters));
}
Debug Mode
- To see all the columns AS identifiers and to pretty print the select part use debugMode: true
var spec = D3Specification.Create<Team>(d3Context)
.And(D3Specification.Create<TeamPlayer>(d3Context))
.And(D3Specification.Create<User>(d3Context))
.And(D3Specification.Create<TournamentTeam>(d3Context))
.And(D3Specification.Create<TournamentTeamStat>(d3Context))
.OrderBy(x => x.Name)
.ExcludeSelectPropMasks(QueryContainer.UserSensitiveData);
spec.ThrowExceptionIfEmptyFilter = false;
D3Specification.BuildSQL(spec, false, true, new Dictionary<int, object?>())
SELECT `
`t0`.`team_id` AS '.TeamId'
, `t0`.`name` AS '.Name'
, `t0`.`logo` AS '.Logo'
, `t0`.`registration_date` AS '.RegistrationDate'
, `t0`.`is_active` AS '.IsActive'
, `t0`.`last_change` AS '.LastChange'
, `t0`.`changed_by` AS '.ChangedBy'
, `t1`.`team_player_id` AS 'TeamPlayers[TeamPlayer].TeamPlayerId'
, `t1`.`team_id` AS 'TeamPlayers[TeamPlayer].TeamId'
, `t1`.`player_id` AS 'TeamPlayers[TeamPlayer].PlayerId'
, `t1`.`last_change` AS 'TeamPlayers[TeamPlayer].LastChange'
, `t1`.`changed_by` AS 'TeamPlayers[TeamPlayer].ChangedBy'
, `t1`.`user_id` AS 'TeamPlayers[TeamPlayer].Player(User).UserId'
, `t1`.`first_name` AS 'TeamPlayers[TeamPlayer].Player(User).FirstName'
, `t1`.`last_name` AS 'TeamPlayers[TeamPlayer].Player(User).LastName'
, `t1`.`gender` AS 'TeamPlayers[TeamPlayer].Player(User).Gender'
, `t1`.`date_of_birth` AS 'TeamPlayers[TeamPlayer].Player(User).DateOfBirth'
, `t1`.`registration_date` AS 'TeamPlayers[TeamPlayer].Player(User).RegistrationDate'
, `t2`.`tournament_team_id` AS 'TournamentTeams[TournamentTeam].TournamentTeamId'
, `t2`.`tournament_id` AS 'TournamentTeams[TournamentTeam].TournamentId'
, `t2`.`team_id` AS 'TournamentTeams[TournamentTeam].TeamId'
, `t2`.`basic_group_name` AS 'TournamentTeams[TournamentTeam].BasicGroupName'
, `t2`.`registration_date` AS 'TournamentTeams[TournamentTeam].RegistrationDate'
, `t2`.`entry_fee_paid` AS 'TournamentTeams[TournamentTeam].EntryFeePaid'
, `t2`.`last_change` AS 'TournamentTeams[TournamentTeam].LastChange'
, `t2`.`changed_by` AS 'TournamentTeams[TournamentTeam].ChangedBy'
, `t2`.`tournament_team_stat_id` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].TournamentTeamStatId'
, `t2`.`tournament_team_id1` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].TournamentTeamId'
, `t2`.`tournament_phase` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].TournamentPhase'
, `t2`.`phase_points` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].PhasePoints'
, `t2`.`wins` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].Wins'
, `t2`.`losts` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].Losts'
, `t2`.`ties` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].Ties'
, `t2`.`sets_won` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].SetsWon'
, `t2`.`sets_lost` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].SetsLost'
, `t2`.`score_plus` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].ScorePlus'
, `t2`.`score_minus` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].ScoreMinus'
, `t2`.`last_change1` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].LastChange'
, `t2`.`changed_by1` AS 'TournamentTeams[TournamentTeam].TournamentTeamStats[TournamentTeamStat].ChangedBy'
FROM `team` AS `t0`
LEFT JOIN (
SELECT `x100`.`team_player_id`, `x100`.`team_id`, `x100`.`player_id`, `x100`.`last_change`, `x100`.`changed_by`, `x100000`.`user_id`, `x100000`.`first_name`, `x100000`.`last_name`, `x100000`.`gender`, `x100000`.`date_of_birth`, `x100000`.`registration_date`
FROM `team_player` AS `x100`
INNER JOIN `user` AS `x100000` ON `x100`.`player_id` = `x100000`.`user_id`
) AS `t1` ON `t0`.`team_id` = `t1`.`team_id`
LEFT JOIN (
SELECT `x101`.`tournament_team_id`, `x101`.`tournament_id`, `x101`.`team_id`, `x101`.`basic_group_name`, `x101`.`registration_date`, `x101`.`entry_fee_paid`, `x101`.`last_change`, `x101`.`changed_by`, `x101100`.`tournament_team_stat_id`, `x101100`.`tournament_team_id` AS `tournament_team_id1`, `x101100`.`tournament_phase`, `x101100`.`phase_points`, `x101100`.`wins`, `x101100`.`losts`, `x101100`.`ties`, `x101100`.`sets_won`, `x101100`.`sets_lost`, `x101100`.`score_plus`, `x101100`.`score_minus`, `x101100`.`last_change` AS `last_change1`, `x101100`.`changed_by` AS `changed_by1`
FROM `tournament_team` AS `x101`
LEFT JOIN `tournament_team_stat` AS `x101100` ON `x101`.`tournament_team_id` = `x101100`.`tournament_team_id`
) AS `t2` ON `t0`.`team_id` = `t2`.`team_id`
-- spec.ThrowExceptionIfEmptyFilter = false;
ORDER BY `t0`.`name`, `t0`.`team_id`, `t1`.`team_player_id`, `t1`.`user_id`, `t2`.`tournament_team_id`
Stored Procedures
- Stored Procedures can be called directly from ICommonRepository as ExecuteStoredSelect(..) or as ExecuteStoredCompute(..)
- You may want to know the naming rules of the columns of stored procedures to work with D3ORM.
- Always use CSharp entities and property names in the AS column identifiers...
- Entity aggregate root name is omitted from the column name, so the root column names looks like '.RootPropertyName'
- Aggregate child must specify all the path of the entity's property you want to assign the value to, excluding entity aggregate root.
- i.e.: 'DirectChild.DirectChildPropertyName' or 'DirectRootChild.GrandParent.Parent.Child.ChildPropertyName'
- If a navigation property name is not the same as a property type, use OneToManyPropertyName[GenericTypeName] for "one to many" relations (collections) or ManyToOnePropertyName(PropertyTypeName) for "many to one" relations.
- (In case of "one to many" relations, you have to do it always even if you do not pluralize the property type, because the property name reflects the entity name it contains, but the type name looks like: ICollection<TEntity> in this case.)
- If you build a query from a collection side and there is no reference to the child query result, use the navigation property like you would specify the inverseRootCollection parameter as following: inverseRootPropertyName[inverseRootCollectionItemType]. <InverseResultColumnName> .
- Compute Aggregation Functions has the format of: '<shortcut> <CSharpPropertyFullPath>'
- List of Compute Shortcuts:
- CNT := COUNT
- AVG := Average
- MIN := Minimal Value
- MAX := Maximal Value
- SUM := SUMA
- i.e.: 'CNT DirectChild.GrandParent.Parent.Child.ChildPropertyName'
If you want to get high performance of the query or you do not want to include columnAsIdentifiers into the query, define them into List<string> and attach them when calling AggSelect(..) or AggCompute(..) or ExecuteStoredSelect(..) or ExecuteStoredCompute(..).
Help yourself by specifying a specification with the same query result structure in order to retrieve the column columnAsIdentifiers and optionally put them into a static or singleton List<string>.
SP Call Expression
// Optionally retrieving column identifiers in order not to define them in a stored procedure itself. We must use an authentic specification for simulation of building the output query of a stored procedure...
var columnAsIdentifiers = D3Specification.BuildSQL(spec, useSkipAndTakeFormat: false, debugMode: true, parameters: spec.Parameters).AsIdentifiersMainSQL;
//
// Select
//
// Note, that skip and take is driven by the stored procedure itself.
//
return Connection.AggSelect<TEntity>(D3Context, inverseRootCollection /*or null*/, inverseRootDirectProperty /*or null*/, storedProcedureName, commandType: CommandType.StoredProcedure, spParameters: new List<DbParameter>() { new DbParameter(...) }, columnAsIdentifiers: columnAsIdentifiers);
//
// Or
//
return SomeD3Repository.ExecuteStoredSelect(storedProcedureName, new List<DbParameter>() { new DbParameter(...) }, columnAsIdentifiers: columnAsIdentifiers);
//
// Compute
//
// Note, that skip and take is driven by the stored procedure itself.
//
return Connection.AggCompute<TEntityOrDTO, TResult /*DTO or object*/>(D3Context, inverseRootCollection /*or null*/, inverseRootDirectProperty /*or null*/, storedProcedureName, commandType: CommandType.StoredProcedure, spParameters: new List<DbParameter>() { new DbParameter(...) }, columnAsIdentifiers: columnAsIdentifiers);
//
// Or
//
return SomeD3Repository.ExecuteStoredCompute<TResult /*DTO or object*/>(storedProcedureName, new List<DbParameter>() { new DbParameter(...) }, columnAsIdentifiers: columnAsIdentifiers);
Parentheses and Correct Alignment
Keep nice alignment of specifications to make life easier...
- Type each specification context to a new line and align it with the tabulator as deep as the level of nesting is.
- You can keep filter or sort functions on the same line except In, NotIn, InFormat, NotInFormat, Not and computing aggregate functions.
- As you can see in the example below, any closing parenthesis on any line end associate a parenthesis of a context start which is nested on the same level as the line after the last closing parenthesis.
- In other words, more closing parentheses at the end of a line means more closed contexts and more level down of nesting on the next line.
/// an example of correct alignment of a specification
ExtendedAllPossibleRootsByMatchBaseQuery
.And(D3Specification.Create < Tournament >(D3Context)
.In(QC.TournamentBaseQuery
.NotIn(y => y.TournamentId, new int[] { 3, 4 }, LogicalOp.AND)
.And(QC.GetTeamIdFilterQuery(null).Compare(y => y.RegistrationDate, ComparisonOp.GREATER_THAN_OR_EQUAL, new DateTime(2021, 1, 1)))))
.And(QC.TeamBaseQuery
.Or(QC.TeamEmptyQuery.Compare(x => x.Name, ComparisonOp.EQUALS, "Team1")
.And(QC.TeamEmptyQuery.Compare(x => x.Name, ComparisonOp.EQUALS, "Team2"))))
.Or(QC.TeamEmptyQuery.Compare(x => x.Name, ComparisonOp.LIKE, "noname%").OrderBy(x => x.Name, true, 1))
.And(D3Specification.Create<Tournament>(D3Context).Compare(x => x.Name, ComparisonOp.LIKE, "noname%", LogicalOp.AND).OrderBy(x => x.Name, false, 2))
.ExcludeSelectPropMasks(nameof(User.PasswordHash))
.Not();
Example of Incorrect Placement of Parentheses
// If you do not switch off exceptions of not finding table relations, the current wrong join will fail... It is a check of correct query building.
D3Specification.ThrowWhenExpectedJoinNotSuccessful = false;
var example = D3Specification.Create<PlayoffRoundCouple>(D3Context).OrderBy(x => x.PlayoffRound, false, 2)
.And(D3Specification.Create(D3Context, (PlayoffRoundCouple couple) => couple.TournamentTeam1).CompareFormat(x => x.TournamentId, ComparisonOp.EQUALS, 0, LogicalOp.AND)
.And(D3Specification.Create<Tournament>(D3Context).OrderBy(x => x.TournamentId, true, 1)) // missing bracket...
//... --> the current context is not PlayoffRoundCouple because of missing bracket => the current context does not contain the property TournamentTeam2 => the next line will be omitted.
.And(D3Specification.Create(D3Context, (PlayoffRoundCouple couple) => couple.TournamentTeam2))) // remaining bracket
.CompareFormat(x => x.PlayoffRound, ComparisonOp.EQUALS, 1, LogicalOp.AND)
.And(D3Specification.Create<Team>(D3Context))
.And(D3Specification.Create<Match>(D3Context).OrderBy(x => x.MatchId))
.And(D3Specification.Create<MatchSetScore>(D3Context).OrderBy(x => x.SetOrder));
-- example SQL
SELECT `
`p0`.`playoff_round_couple_id` AS '.PlayoffRoundCoupleId'
, `p0`.`tournament_team1_id` AS '.TournamentTeam1Id'
, `p0`.`tournament_team2_id` AS '.TournamentTeam2Id'
, `p0`.`playoff_round` AS '.PlayoffRound'
, `p0`.`team1_wins` AS '.Team1Wins'
, `p0`.`team2_wins` AS '.Team2Wins'
, `p0`.`last_change` AS '.LastChange'
, `p0`.`changed_by` AS '.ChangedBy'
, `t0`.`tournament_team_id` AS 'TournamentTeam1(TournamentTeam).TournamentTeamId'
, `t0`.`tournament_id` AS 'TournamentTeam1(TournamentTeam).TournamentId'
, `t0`.`team_id` AS 'TournamentTeam1(TournamentTeam).TeamId'
, `t0`.`basic_group_name` AS 'TournamentTeam1(TournamentTeam).BasicGroupName'
, `t0`.`registration_date` AS 'TournamentTeam1(TournamentTeam).RegistrationDate'
, `t0`.`entry_fee_paid` AS 'TournamentTeam1(TournamentTeam).EntryFeePaid'
, `t0`.`last_change` AS 'TournamentTeam1(TournamentTeam).LastChange'
, `t0`.`changed_by` AS 'TournamentTeam1(TournamentTeam).ChangedBy'
, `t1`.`tournament_id` AS 'TournamentTeam1(TournamentTeam).Tournament.TournamentId'
, `t1`.`tour_serie_id` AS 'TournamentTeam1(TournamentTeam).Tournament.TourSerieId'
, `t1`.`address_id` AS 'TournamentTeam1(TournamentTeam).Tournament.AddressId'
, `t1`.`name` AS 'TournamentTeam1(TournamentTeam).Tournament.Name'
, `t1`.`start_date` AS 'TournamentTeam1(TournamentTeam).Tournament.StartDate'
, `t1`.`end_date` AS 'TournamentTeam1(TournamentTeam).Tournament.EndDate'
, `t1`.`entry_fee` AS 'TournamentTeam1(TournamentTeam).Tournament.EntryFee'
, `t1`.`max_num_of_teams` AS 'TournamentTeam1(TournamentTeam).Tournament.MaxNumOfTeams'
, `t1`.`note` AS 'TournamentTeam1(TournamentTeam).Tournament.Note'
, `t1`.`last_change` AS 'TournamentTeam1(TournamentTeam).Tournament.LastChange'
, `t1`.`changed_by` AS 'TournamentTeam1(TournamentTeam).Tournament.ChangedBy'
, `t2`.`team_id` AS 'TournamentTeam1(TournamentTeam).Team.TeamId'
, `t2`.`name` AS 'TournamentTeam1(TournamentTeam).Team.Name'
, `t2`.`logo` AS 'TournamentTeam1(TournamentTeam).Team.Logo'
, `t2`.`registration_date` AS 'TournamentTeam1(TournamentTeam).Team.RegistrationDate'
, `t2`.`is_active` AS 'TournamentTeam1(TournamentTeam).Team.IsActive'
, `t2`.`last_change` AS 'TournamentTeam1(TournamentTeam).Team.LastChange'
, `t2`.`changed_by` AS 'TournamentTeam1(TournamentTeam).Team.ChangedBy'
-- omitted query items
-- , `t3`.`tournament_team_id` AS 'TournamentTeam2(TournamentTeam).TournamentTeamId'
-- , `t3`.`tournament_id` AS 'TournamentTeam2(TournamentTeam).TournamentId'
-- , `t3`.`team_id` AS 'TournamentTeam2(TournamentTeam).TeamId'
-- , `t3`.`basic_group_name` AS 'TournamentTeam2(TournamentTeam).BasicGroupName'
-- , `t3`.`registration_date` AS 'TournamentTeam2(TournamentTeam).RegistrationDate'
-- , `t3`.`entry_fee_paid` AS 'TournamentTeam2(TournamentTeam).EntryFeePaid'
-- , `t3`.`last_change` AS 'TournamentTeam2(TournamentTeam).LastChange'
-- , `t3`.`changed_by` AS 'TournamentTeam2(TournamentTeam).ChangedBy'
-- , `t4`.`team_id` AS 'TournamentTeam2(TournamentTeam).Team.TeamId'
-- , `t4`.`name` AS 'TournamentTeam2(TournamentTeam).Team.Name'
-- , `t4`.`logo` AS 'TournamentTeam2(TournamentTeam).Team.Logo'
-- , `t4`.`registration_date` AS 'TournamentTeam2(TournamentTeam).Team.RegistrationDate'
-- , `t4`.`is_active` AS 'TournamentTeam2(TournamentTeam).Team.IsActive'
-- , `t4`.`last_change` AS 'TournamentTeam2(TournamentTeam).Team.LastChange'
-- , `t4`.`changed_by` AS 'TournamentTeam2(TournamentTeam).Team.ChangedBy'
, `m0`.`match_id` AS 'Matches[Match].MatchId'
, `m0`.`home_team_id` AS 'Matches[Match].HomeTeamId'
, `m0`.`away_team_id` AS 'Matches[Match].AwayTeamId'
, `m0`.`tournament_id` AS 'Matches[Match].TournamentId'
, `m0`.`tournament_phase` AS 'Matches[Match].TournamentPhase'
, `m0`.`winner_id` AS 'Matches[Match].WinnerId'
, `m0`.`referee_id` AS 'Matches[Match].RefereeId'
, `m0`.`match_date` AS 'Matches[Match].MatchDate'
, `m0`.`playoff_round_couple_id` AS 'Matches[Match].PlayoffRoundCoupleId'
, `m0`.`last_change` AS 'Matches[Match].LastChange'
, `m0`.`changed_by` AS 'Matches[Match].ChangedBy'
, `m0`.`match_set_score_id` AS 'Matches[Match].MatchSetScores[MatchSetScore].MatchSetScoreId'
, `m0`.`match_id1` AS 'Matches[Match].MatchSetScores[MatchSetScore].MatchId'
, `m0`.`home_team_score` AS 'Matches[Match].MatchSetScores[MatchSetScore].HomeTeamScore'
, `m0`.`away_team_score` AS 'Matches[Match].MatchSetScores[MatchSetScore].AwayTeamScore'
, `m0`.`set_order` AS 'Matches[Match].MatchSetScores[MatchSetScore].SetOrder'
, `m0`.`last_change1` AS 'Matches[Match].MatchSetScores[MatchSetScore].LastChange'
, `m0`.`changed_by1` AS 'Matches[Match].MatchSetScores[MatchSetScore].ChangedBy'
FROM ...