using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// /// Represents the UserRepository for doing CRUD operations for /// internal class UserRepository : EntityRepositoryBase, IUserRepository { private readonly IMapperCollection _mapperCollection; private readonly GlobalSettings _globalSettings; private readonly UserPasswordConfigurationSettings _passwordConfiguration; private readonly IJsonSerializer _jsonSerializer; private string _passwordConfigJson; private bool _passwordConfigInitialized; /// /// Constructor /// /// /// /// /// /// A dictionary specifying the configuration for user passwords. If this is null then no password configuration will be persisted or read. /// /// public UserRepository( IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IMapperCollection mapperCollection, IOptions globalSettings, IOptions passwordConfiguration, IJsonSerializer jsonSerializer) : base(scopeAccessor, appCaches, logger) { _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); _passwordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); _jsonSerializer = jsonSerializer; } /// /// Returns a serialized dictionary of the password configuration that is stored against the user in the database /// private string DefaultPasswordConfigJson { get { if (_passwordConfigInitialized) return _passwordConfigJson; var passwordConfig = new UserPasswordSettings { HashAlgorithm = _passwordConfiguration.HashAlgorithmType }; _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); _passwordConfigInitialized = true; return _passwordConfigJson; } } #region Overrides of RepositoryBase protected override IUser PerformGet(int id) { var sql = SqlContext.Sql() .Select() .From() .Where(x => x.Id == id); var dtos = Database.Fetch(sql); if (dtos.Count == 0) return null; PerformGetReferencedDtos(dtos); return UserFactory.BuildEntity(_globalSettings, dtos[0]); } /// /// Returns a user by username /// /// /// /// Can be used for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes). /// This is really only used for a shim in order to upgrade to 7.6. /// /// /// A non cached instance /// public IUser GetByUsername(string username, bool includeSecurityData) { return GetWith(sql => sql.Where(x => x.Login == username), includeSecurityData); } /// /// Returns a user by id /// /// /// /// This is really only used for a shim in order to upgrade to 7.6 but could be used /// for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes) /// /// /// A non cached instance /// public IUser Get(int id, bool includeSecurityData) { return GetWith(sql => sql.Where(x => x.Id == id), includeSecurityData); } public IProfile GetProfile(string username) { var dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); return dto == null ? null : new UserProfile(dto.Id, dto.UserName); } public IProfile GetProfile(int id) { var dto = GetDtoWith(sql => sql.Where(x => x.Id == id), false); return dto == null ? null : new UserProfile(dto.Id, dto.UserName); } public IDictionary GetUserStates() { var sql = @"SELECT '1CountOfAll' AS colName, COUNT(id) AS num FROM umbracoUser UNION SELECT '2CountOfActive' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL UNION SELECT '3CountOfDisabled' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 1 UNION SELECT '4CountOfLockedOut' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userNoConsole = 1 UNION SELECT '5CountOfInvited' AS colName, COUNT(id) AS num FROM umbracoUser WHERE lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL UNION SELECT '6CountOfDisabled' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL ORDER BY colName"; var result = Database.Fetch(sql); return new Dictionary { {UserState.All, (int) result[0].num}, {UserState.Active, (int) result[1].num}, {UserState.Disabled, (int) result[2].num}, {UserState.LockedOut, (int) result[3].num}, {UserState.Invited, (int) result[4].num}, {UserState.Inactive, (int) result[5].num} }; } public Guid CreateLoginSession(int userId, string requestingIpAddress, bool cleanStaleSessions = true) { var now = DateTime.UtcNow; var dto = new UserLoginDto { UserId = userId, IpAddress = requestingIpAddress, LoggedInUtc = now, LastValidatedUtc = now, LoggedOutUtc = null, SessionId = Guid.NewGuid() }; Database.Insert(dto); if (cleanStaleSessions) { ClearLoginSessions(TimeSpan.FromDays(15)); } return dto.SessionId; } public bool ValidateLoginSession(int userId, Guid sessionId) { // with RepeatableRead transaction mode, read-then-update operations can // cause deadlocks, and the ForUpdate() hint is required to tell the database // to acquire an exclusive lock when reading // that query is going to run a *lot*, make it a template var t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s .Select() .SelectTop(1) .From() .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) .ForUpdate()); var sql = t.Sql(sessionId); var found = Database.FirstOrDefault(sql); if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) return false; //now detect if there's been a timeout if (DateTime.UtcNow - found.LastValidatedUtc > TimeSpan.FromMinutes(_globalSettings.TimeOutInMinutes)) { //timeout detected, update the record ClearLoginSession(sessionId); return false; } //update the validate date found.LastValidatedUtc = DateTime.UtcNow; Database.Update(found); return true; } public int ClearLoginSessions(int userId) { return Database.Delete(Sql().Where(x => x.UserId == userId)); } public int ClearLoginSessions(TimeSpan timespan) { var fromDate = DateTime.UtcNow - timespan; return Database.Delete(Sql().Where(x => x.LastValidatedUtc < fromDate)); } public void ClearLoginSession(Guid sessionId) { // TODO: why is that one updating and not deleting? Database.Execute(Sql() .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow)) .Where(x => x.SessionId == sessionId)); } protected override IEnumerable PerformGetAll(params int[] ids) { var dtos = ids.Length == 0 ? GetDtosWith(null, true) : GetDtosWith(sql => sql.WhereIn(x => x.Id, ids), true); var users = new IUser[dtos.Count]; var i = 0; foreach (var dto in dtos) users[i++] = UserFactory.BuildEntity(_globalSettings, dto); return users; } protected override IEnumerable PerformGetByQuery(IQuery query) { var dtos = GetDtosWith(sql => new SqlTranslator(sql, query).Translate(), true) .DistinctBy(x => x.Id) .ToList(); var users = new IUser[dtos.Count]; var i = 0; foreach (var dto in dtos) users[i++] = UserFactory.BuildEntity(_globalSettings, dto); return users; } private IUser GetWith(Action> with, bool includeReferences) { var dto = GetDtoWith(with, includeReferences); return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto); } private UserDto GetDtoWith(Action> with, bool includeReferences) { var dtos = GetDtosWith(with, includeReferences); return dtos.FirstOrDefault(); } private List GetDtosWith(Action> with, bool includeReferences) { var sql = SqlContext.Sql() .Select() .From(); with?.Invoke(sql); var dtos = Database.Fetch(sql); if (includeReferences) PerformGetReferencedDtos(dtos); return dtos; } // NPoco cannot fetch 2+ references at a time // plus it creates a combinatorial explosion // better use extra queries // unfortunately, SqlCe doesn't support multiple result sets private void PerformGetReferencedDtos(List dtos) { if (dtos.Count == 0) return; var userIds = dtos.Count == 1 ? new List { dtos[0].Id } : dtos.Select(x => x.Id).ToList(); var xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x); // get users2groups var sql = SqlContext.Sql() .Select() .From() .WhereIn(x => x.UserId, userIds); var users2groups = Database.Fetch(sql); var groupIds = users2groups.Select(x => x.UserGroupId).ToList(); // get groups sql = SqlContext.Sql() .Select() .From() .WhereIn(x => x.Id, groupIds); var groups = Database.Fetch(sql) .ToDictionary(x => x.Id, x => x); // get groups2apps sql = SqlContext.Sql() .Select() .From() .WhereIn(x => x.UserGroupId, groupIds); var groups2apps = Database.Fetch(sql) .GroupBy(x => x.UserGroupId) .ToDictionary(x => x.Key, x => x); // get start nodes sql = SqlContext.Sql() .Select() .From() .WhereIn(x => x.UserId, userIds); var startNodes = Database.Fetch(sql); // map groups foreach (var user2group in users2groups) { if (groups.TryGetValue(user2group.UserGroupId, out var group)) { var dto = xUsers == null ? dtos[0] : xUsers[user2group.UserId]; dto.UserGroupDtos.Add(group); // user2group is distinct } } // map start nodes foreach (var startNode in startNodes) { var dto = xUsers == null ? dtos[0] : xUsers[startNode.UserId]; dto.UserStartNodeDtos.Add(startNode); // hashset = distinct } // map apps foreach (var group in groups.Values) { if (groups2apps.TryGetValue(group.Id, out var list)) group.UserGroup2AppDtos = list.ToList(); // groups2apps is distinct } } #endregion #region Overrides of EntityRepositoryBase protected override Sql GetBaseQuery(bool isCount) { if (isCount) return SqlContext.Sql() .SelectCount() .From(); return SqlContext.Sql() .Select() .From(); } private static void AddGroupLeftJoin(Sql sql) { sql .LeftJoin() .On(left => left.UserId, right => right.Id) .LeftJoin() .On(left => left.Id, right => right.UserGroupId) .LeftJoin() .On(left => left.UserGroupId, right => right.Id) .LeftJoin() .On(left => left.UserId, right => right.Id); } private Sql GetBaseQuery(string columns) { return SqlContext.Sql() .Select(columns) .From(); } protected override string GetBaseWhereClause() { return "umbracoUser.id = @id"; } protected override IEnumerable GetDeleteClauses() { var list = new List { "DELETE FROM umbracoUser2UserGroup WHERE userId = @id", "DELETE FROM umbracoUser2NodeNotify WHERE userId = @id", "DELETE FROM umbracoUserStartNode WHERE userId = @id", "DELETE FROM umbracoUser WHERE id = @id", "DELETE FROM umbracoExternalLogin WHERE id = @id" }; return list; } protected override Guid NodeObjectTypeId => throw new NotImplementedException(); protected override void PersistNewItem(IUser entity) { // the use may have no identity, ie ID is zero, and be v7 super // user - then it has been marked - and we must not persist it // as new, as we do not want to create a new user - instead, persist // it as updated // see also: UserFactory.BuildEntity if (entity.FromUserCache("IS_V7_ZERO") != null) { PersistUpdatedItem(entity); return; } entity.AddingEntity(); // ensure security stamp if missing if (entity.SecurityStamp.IsNullOrWhiteSpace()) entity.SecurityStamp = Guid.NewGuid().ToString(); var userDto = UserFactory.BuildDto(entity); // check if we have a user config else use the default userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; var id = Convert.ToInt32(Database.Insert(userDto)); entity.Id = id; if (entity.IsPropertyDirty("StartContentIds")) { AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); } if (entity.IsPropertyDirty("StartMediaIds")) { AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); } if (entity.IsPropertyDirty("Groups")) { // lookup all assigned var assigned = entity.Groups == null || entity.Groups.Any() == false ? new List() : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); foreach (var groupDto in assigned) { var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id }; Database.Insert(dto); } } entity.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IUser entity) { // updates Modified date entity.UpdatingEntity(); // ensure security stamp if missing if (entity.SecurityStamp.IsNullOrWhiteSpace()) entity.SecurityStamp = Guid.NewGuid().ToString(); var userDto = UserFactory.BuildDto(entity); // build list of columns to check for saving - we don't want to save the password if it hasn't changed! // list the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that var colsToSave = new Dictionary { //TODO: Change these to constants + nameof {"userDisabled", "IsApproved"}, {"userNoConsole", "IsLockedOut"}, {"startStructureID", "StartContentId"}, {"startMediaID", "StartMediaId"}, {"userName", "Name"}, {"userLogin", "Username"}, {"userEmail", "Email"}, {"userLanguage", "Language"}, {"securityStampToken", "SecurityStamp"}, {"lastLockoutDate", "LastLockoutDate"}, {"lastPasswordChangeDate", "LastPasswordChangeDate"}, {"lastLoginDate", "LastLoginDate"}, {"failedLoginAttempts", "FailedPasswordAttempts"}, {"createDate", "CreateDate"}, {"updateDate", "UpdateDate"}, {"avatar", "Avatar"}, {"emailConfirmedDate", "EmailConfirmedDate"}, {"invitedDate", "InvitedDate"}, {"tourData", "TourData"} }; // create list of properties that have changed var changedCols = colsToSave .Where(col => entity.IsPropertyDirty(col.Value)) .Select(col => col.Key) .ToList(); // DO NOT update the password if it has not changed or if it is null or empty if (entity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) { changedCols.Add("userPassword"); // special case - when using ASP.Net identity the user manager will take care of updating the security stamp, however // when not using ASP.Net identity (i.e. old membership providers), we'll need to take care of updating this manually // so we can just detect if that property is dirty, if it's not we'll set it manually if (entity.IsPropertyDirty("SecurityStamp") == false) { userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); changedCols.Add("securityStampToken"); } // check if we have a user config else use the default userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; changedCols.Add("passwordConfig"); } // If userlogin or the email has changed then need to reset security stamp if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) { userDto.EmailConfirmedDate = null; userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); changedCols.Add("emailConfirmedDate"); changedCols.Add("securityStampToken"); } //only update the changed cols if (changedCols.Count > 0) { Database.Update(userDto, changedCols); } if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) { var assignedStartNodes = Database.Fetch("SELECT * FROM umbracoUserStartNode WHERE userId = @userId", new { userId = entity.Id }); if (entity.IsPropertyDirty("StartContentIds")) { AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); } if (entity.IsPropertyDirty("StartMediaIds")) { AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); } } if (entity.IsPropertyDirty("Groups")) { //lookup all assigned var assigned = entity.Groups == null || entity.Groups.Any() == false ? new List() : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); //first delete all // TODO: We could do this a nicer way instead of "Nuke and Pave" Database.Delete("WHERE UserId = @UserId", new { UserId = entity.Id }); foreach (var groupDto in assigned) { var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id }; Database.Insert(dto); } } entity.ResetDirtyProperties(); } private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, UserStartNodeDto.StartNodeTypeValue startNodeType, int[] entityStartIds) { var assignedIds = current.Where(x => x.StartNodeType == (int)startNodeType).Select(x => x.StartNode).ToArray(); //remove the ones not assigned to the entity var toDelete = assignedIds.Except(entityStartIds).ToArray(); if (toDelete.Length > 0) Database.Delete("WHERE UserId = @UserId AND startNode IN (@startNodes)", new { UserId = entity.Id, startNodes = toDelete }); //add the ones not currently in the db var toAdd = entityStartIds.Except(assignedIds).ToArray(); foreach (var i in toAdd) { var dto = new UserStartNodeDto { StartNode = i, StartNodeType = (int)startNodeType, UserId = entity.Id }; Database.Insert(dto); } } #endregion #region Implementation of IUserRepository public int GetCountByQuery(IQuery query) { var sqlClause = GetBaseQuery("umbracoUser.id"); var translator = new SqlTranslator(sqlClause, query); var subquery = translator.Translate(); //get the COUNT base query var sql = GetBaseQuery(true) .Append(new Sql("WHERE umbracoUser.id IN (" + subquery.SQL + ")", subquery.Arguments)); return Database.ExecuteScalar(sql); } public bool Exists(string username) { return ExistsByUserName(username); } public bool ExistsByUserName(string username) { var sql = SqlContext.Sql() .SelectCount() .From() .Where(x => x.UserName == username); return Database.ExecuteScalar(sql) > 0; } public bool ExistsByLogin(string login) { var sql = SqlContext.Sql() .SelectCount() .From() .Where(x => x.Login == login); return Database.ExecuteScalar(sql) > 0; } /// /// Gets a list of objects associated with a given group /// /// Id of group public IEnumerable GetAllInGroup(int groupId) { return GetAllInOrNotInGroup(groupId, true); } /// /// Gets a list of objects not associated with a given group /// /// Id of group public IEnumerable GetAllNotInGroup(int groupId) { return GetAllInOrNotInGroup(groupId, false); } private IEnumerable GetAllInOrNotInGroup(int groupId, bool include) { var sql = SqlContext.Sql() .Select() .From(); var inSql = SqlContext.Sql() .Select(x => x.UserId) .From() .Where(x => x.UserGroupId == groupId); if (include) sql.WhereIn(x => x.Id, inSql); else sql.WhereNotIn(x => x.Id, inSql); var dtos = Database.Fetch(sql); //adds missing bits like content and media start nodes PerformGetReferencedDtos(dtos); return ConvertFromDtos(dtos); } /// /// Gets paged user results /// /// /// /// /// /// /// /// /// A filter to only include user that belong to these user groups /// /// /// A filter to only include users that do not belong to these user groups /// /// Optional parameter to filter by specified user state /// /// /// /// The query supplied will ONLY work with data specifically on the umbracoUser table because we are using NPoco paging (SQL paging) /// public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Expression> orderBy, Direction orderDirection = Direction.Ascending, string[] includeUserGroups = null, string[] excludeUserGroups = null, UserState[] userState = null, IQuery filter = null) { if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); Sql filterSql = null; var customFilterWheres = filter?.GetWhereClauses().ToArray(); var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; if (hasCustomFilter || includeUserGroups != null && includeUserGroups.Length > 0 || excludeUserGroups != null && excludeUserGroups.Length > 0 || userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false) filterSql = SqlContext.Sql(); if (hasCustomFilter) { foreach (var clause in customFilterWheres) filterSql.Append($"AND ({clause.Item1})", clause.Item2); } if (includeUserGroups != null && includeUserGroups.Length > 0) { const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id FROM umbracoUser INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; filterSql.Append(subQuery, new { userGroups = includeUserGroups }); } if (excludeUserGroups != null && excludeUserGroups.Length > 0) { const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id FROM umbracoUser INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; filterSql.Append(subQuery, new { userGroups = excludeUserGroups }); } if (userState != null && userState.Length > 0) { //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering if (userState.Contains(UserState.All) == false) { var sb = new StringBuilder("("); var appended = false; if (userState.Contains(UserState.Active)) { sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); appended = true; } if (userState.Contains(UserState.Inactive)) { if (appended) sb.Append(" OR "); sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL)"); appended = true; } if (userState.Contains(UserState.Disabled)) { if (appended) sb.Append(" OR "); sb.Append("(userDisabled = 1)"); appended = true; } if (userState.Contains(UserState.LockedOut)) { if (appended) sb.Append(" OR "); sb.Append("(userNoConsole = 1)"); appended = true; } if (userState.Contains(UserState.Invited)) { if (appended) sb.Append(" OR "); sb.Append("(lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL)"); appended = true; } sb.Append(")"); filterSql.Append("AND " + sb); } } // create base query var sql = SqlContext.Sql() .Select() .From(); // apply query if (query != null) sql = new SqlTranslator(sql, query).Translate(); // get sorted and filtered sql var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection); // get a page of results and total count var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); totalRecords = Convert.ToInt32(pagedResult.TotalItems); // map references PerformGetReferencedDtos(pagedResult.Items); return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x)); } private Sql ApplyFilter(Sql sql, Sql filterSql, bool hasWhereClause) { if (filterSql == null) return sql; //ensure we don't append a WHERE if there is already one var args = filterSql.Arguments; var sqlFilter = hasWhereClause ? filterSql.SQL : " WHERE " + filterSql.SQL.TrimStart("AND "); sql.Append(SqlContext.Sql(sqlFilter, args)); return sql; } private Sql ApplySort(Sql sql, Expression> orderBy, Direction orderDirection) { if (orderBy == null) return sql; var expressionMember = ExpressionHelper.GetMemberInfo(orderBy); var mapper = _mapperCollection[typeof(IUser)]; var mappedField = mapper.Map(expressionMember.Name); if (mappedField.IsNullOrWhiteSpace()) throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); // beware! NPoco paging code parses the query to isolate the ORDER BY fragment, // using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything // else in orderBy is going to break NPoco / not be detected // beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar] // to [bar] only, so we MUST use aliases, cannot use [table].[field] // beware! pre-2012 SqlServer is using a convoluted syntax for paging, which // includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...", // so anything added here MUST also be part of the inner SELECT statement, ie // the original statement, AND must be using the proper alias, as the inner SELECT // will hide the original table.field names entirely var orderByField = sql.GetAliasedField(mappedField); if (orderDirection == Direction.Ascending) sql.OrderBy(orderByField); else sql.OrderByDescending(orderByField); return sql; } internal IEnumerable GetNextUsers(int id, int count) { var idsQuery = SqlContext.Sql() .Select(x => x.Id) .From() .Where(x => x.Id >= id) .OrderBy(x => x.Id); // first page is index 1, not zero var ids = Database.Page(1, count, idsQuery).Items.ToArray(); // now get the actual users and ensure they are ordered properly (same clause) return ids.Length == 0 ? Enumerable.Empty() : GetMany(ids).OrderBy(x => x.Id); } #endregion private IEnumerable ConvertFromDtos(IEnumerable dtos) { return dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x)); } } }