diff --git a/packages/adapter-commons/src/declarations.ts b/packages/adapter-commons/src/declarations.ts
index 0709046f3d..6ac368e1a2 100644
--- a/packages/adapter-commons/src/declarations.ts
+++ b/packages/adapter-commons/src/declarations.ts
@@ -1,7 +1,15 @@
 import { Query, Params, Paginated, Id, NullableId } from '@feathersjs/feathers';
 
-export type FilterSettings = string[]|{
-  [key: string]: (value: any, options: any) => any
+export type FilterQueryOptions = {
+  filters?: FilterSettings;
+  operators?: string[];
+  paginate?: PaginationParams;
+}
+
+export type QueryFilter = (value: any, options: FilterQueryOptions) => any
+
+export type FilterSettings = {
+  [key: string]: QueryFilter | true
 }
 
 export interface PaginationOptions {
@@ -9,28 +17,41 @@ export interface PaginationOptions {
   max?: number;
 }
 
-export type PaginationParams = false|PaginationOptions;
-
-export type FilterQueryOptions = {
-  filters?: FilterSettings;
-  operators?: string[];
-  paginate?: PaginationParams;
-}
+export type PaginationParams = false | PaginationOptions;
 
 export interface AdapterServiceOptions {
-  events?: string[];
-  multi?: boolean|string[];
+  /**
+   * Whether to allow multiple updates for everything (`true`) or specific methods (e.g. `['create', 'remove']`)
+   */
+  multi?: boolean | string[];
+  /**
+   * The name of the id property
+   */
   id?: string;
+  /**
+   * Pagination settings for this service
+   */
   paginate?: PaginationOptions
   /**
-   * @deprecated renamed to `allow`.
+   * A list of additional property query operators to allow in a query
    */
+  operators?: string[];
+  /**
+   * An object of additional top level query filters, e.g. `{ $populate: true }`
+   * Can also be a converter function like `{ $ignoreCase: (value) => value === 'true' ? true : false }`
+   */
+  filters?: FilterSettings;
+  /**
+   * @deprecated Use service `events` option when registering the service with `app.use`.
+   */
+  events?: string[];
+  /**
+ * @deprecated renamed to `operators`.
+ */
   whitelist?: string[];
-  allow?: string[];
-  filters?: string[];
 }
 
-export interface AdapterOptions<M = any> extends Pick<AdapterServiceOptions, 'multi'|'allow'|'paginate'> {
+export interface AdapterOptions<M = any> extends Pick<AdapterServiceOptions, 'multi' | 'filters' | 'operators' | 'paginate'> {
   Model?: M;
 }
 
@@ -41,7 +62,7 @@ export interface AdapterParams<Q = Query, M = any> extends Params<Q> {
 
 /**
  * Hook-less (internal) service methods. Directly call database adapter service methods
- * without running any service-level hooks. This can be useful if you need the raw data
+ * without running any service-level hooks or sanitization. This can be useful if you need the raw data
  * from the service and don't want to trigger any of its hooks.
  *
  * Important: These methods are only available internally on the server, not on the client
@@ -51,42 +72,44 @@ export interface AdapterParams<Q = Query, M = any> extends Params<Q> {
  *
  * @see {@link https://docs.feathersjs.com/guides/migrating.html#hook-less-service-methods}
  */
- export interface InternalServiceMethods<T = any, D = Partial<T>, P extends AdapterParams = AdapterParams> {
+export interface InternalServiceMethods<T = any, D = Partial<T>, P extends AdapterParams = AdapterParams> {
   /**
-   * Retrieve all resources from this service, skipping any service-level hooks.
+   * Retrieve all resources from this service.
+   * Does not sanitize the query and should only be used on the server.
    *
-   * @param params - Service call parameters {@link Params}
-   * @see {@link HookLessServiceMethods}
-   * @see {@link https://docs.feathersjs.com/api/services.html#find-params|Feathers API Documentation: .find(params)}
+   * @param _params - Service call parameters {@link Params}
    */
-  _find (_params?: P & { paginate?: PaginationOptions }): Promise<Paginated<T>>;
-  _find (_params?: P & { paginate: false }): Promise<T[]>;
-  _find (params?: P): Promise<T | T[] | Paginated<T>>;
+  $find(_params?: P & { paginate?: PaginationOptions }): Promise<Paginated<T>>;
+  $find(_params?: P & { paginate: false }): Promise<T[]>;
+  $find(params?: P): Promise<T[] | Paginated<T>>;
 
   /**
    * Retrieve a single resource matching the given ID, skipping any service-level hooks.
+   * Does not sanitize the query and should only be used on the server.
    *
    * @param id - ID of the resource to locate
    * @param params - Service call parameters {@link Params}
    * @see {@link HookLessServiceMethods}
    * @see {@link https://docs.feathersjs.com/api/services.html#get-id-params|Feathers API Documentation: .get(id, params)}
    */
-  _get (id: Id, params?: P): Promise<T>;
+  $get(id: Id, params?: P): Promise<T>;
 
   /**
    * Create a new resource for this service, skipping any service-level hooks.
+   * Does not sanitize data or checks if multiple updates are allowed and should only be used on the server.
    *
    * @param data - Data to insert into this service.
    * @param params - Service call parameters {@link Params}
    * @see {@link HookLessServiceMethods}
    * @see {@link https://docs.feathersjs.com/api/services.html#create-data-params|Feathers API Documentation: .create(data, params)}
    */
-   _create (data: Partial<D>, params?: P): Promise<T>;
-   _create (data: Partial<D>[], params?: P): Promise<T[]>;
-   _create (data: Partial<D>|Partial<D>[], params?: P): Promise<T|T[]>;
+  $create(data: Partial<D>, params?: P): Promise<T>;
+  $create(data: Partial<D>[], params?: P): Promise<T[]>;
+  $create(data: Partial<D> | Partial<D>[], params?: P): Promise<T | T[]>;
 
   /**
-   * Replace any resources matching the given ID with the given data, skipping any service-level hooks.
+   * Completely replace the resource identified by id, skipping any service-level hooks.
+   * Does not sanitize data or query and should only be used on the server.
    *
    * @param id - ID of the resource to be updated
    * @param data - Data to be put in place of the current resource.
@@ -94,10 +117,11 @@ export interface AdapterParams<Q = Query, M = any> extends Params<Q> {
    * @see {@link HookLessServiceMethods}
    * @see {@link https://docs.feathersjs.com/api/services.html#update-id-data-params|Feathers API Documentation: .update(id, data, params)}
    */
-  _update (id: Id, data: D, params?: P): Promise<T>;
+  $update(id: Id, data: D, params?: P): Promise<T>;
 
   /**
    * Merge any resources matching the given ID with the given data, skipping any service-level hooks.
+   * Does not sanitize the data or query and should only be used on the server.
    *
    * @param id - ID of the resource to be patched
    * @param data - Data to merge with the current resource.
@@ -105,19 +129,20 @@ export interface AdapterParams<Q = Query, M = any> extends Params<Q> {
    * @see {@link HookLessServiceMethods}
    * @see {@link https://docs.feathersjs.com/api/services.html#patch-id-data-params|Feathers API Documentation: .patch(id, data, params)}
    */
-  _patch (id: null, data: Partial<D>, params?: P): Promise<T[]>;
-  _patch (id: Id, data: Partial<D>, params?: P): Promise<T>;
-  _patch (id: NullableId, data: Partial<D>, params?: P): Promise<T|T[]>;
+  $patch(id: null, data: Partial<D>, params?: P): Promise<T[]>;
+  $patch(id: Id, data: Partial<D>, params?: P): Promise<T>;
+  $patch(id: NullableId, data: Partial<D>, params?: P): Promise<T | T[]>;
 
   /**
    * Remove resources matching the given ID from the this service, skipping any service-level hooks.
+   * Does not sanitize query and should only be used on the server.
    *
    * @param id - ID of the resource to be removed
    * @param params - Service call parameters {@link Params}
    * @see {@link HookLessServiceMethods}
    * @see {@link https://docs.feathersjs.com/api/services.html#remove-id-params|Feathers API Documentation: .remove(id, params)}
    */
-  _remove (id: null, params?: P): Promise<T[]>;
-  _remove (id: Id, params?: P): Promise<T>;
-  _remove (id: NullableId, params?: P): Promise<T|T[]>;
+   $remove(id: null, params?: P): Promise<T[]>;
+   $remove(id: Id, params?: P): Promise<T>;
+   $remove(id: NullableId, params?: P): Promise<T | T[]>;
 }
\ No newline at end of file
diff --git a/packages/adapter-commons/src/filter-query.ts b/packages/adapter-commons/src/filter-query.ts
deleted file mode 100644
index 4cc29e4472..0000000000
--- a/packages/adapter-commons/src/filter-query.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import { _ } from '@feathersjs/commons';
-import { BadRequest } from '@feathersjs/errors';
-import { Query } from '@feathersjs/feathers';
-import { FilterQueryOptions, FilterSettings } from './declarations';
-
-function parse (number: any) {
-  if (typeof number !== 'undefined') {
-    return Math.abs(parseInt(number, 10));
-  }
-
-  return undefined;
-}
-
-// Returns the pagination limit and will take into account the
-// default and max pagination settings
-function getLimit (limit: any, paginate: any) {
-  if (paginate && (paginate.default || paginate.max)) {
-    const base = paginate.default || 0;
-    const lower = typeof limit === 'number' && !isNaN(limit) ? limit : base;
-    const upper = typeof paginate.max === 'number' ? paginate.max : Number.MAX_VALUE;
-
-    return Math.min(lower, upper);
-  }
-
-  return limit;
-}
-
-// Makes sure that $sort order is always converted to an actual number
-function convertSort (sort: any) {
-  if (typeof sort !== 'object' || Array.isArray(sort)) {
-    return sort;
-  }
-
-  return Object.keys(sort).reduce((result, key) => {
-    result[key] = typeof sort[key] === 'object'
-      ? sort[key] : parseInt(sort[key], 10);
-
-    return result;
-  }, {} as { [key: string]: number });
-}
-
-function cleanQuery (query: Query, operators: any, filters: any): any {
-  if (Array.isArray(query)) {
-    return query.map(value => cleanQuery(value, operators, filters));
-  } else if (_.isObject(query) && query.constructor === {}.constructor) {
-    const result: { [key: string]: any } = {};
-
-    _.each(query, (value, key) => {
-      if (key[0] === '$') {
-        if (filters[key] !== undefined) {
-          return;
-        }
-
-        if (!operators.includes(key)) {
-          throw new BadRequest(`Invalid query parameter ${key}`, query);
-        }
-      }
-
-      result[key] = cleanQuery(value, operators, filters);
-    });
-
-    Object.getOwnPropertySymbols(query).forEach(symbol => {
-      // @ts-ignore
-      result[symbol] = query[symbol];
-    });
-
-    return result;
-  }
-
-  return query;
-}
-
-function assignFilters (object: any, query: Query, filters: FilterSettings, options: any): { [key: string]: any } {
-  if (Array.isArray(filters)) {
-    _.each(filters, (key) => {
-      if (query[key] !== undefined) {
-        object[key] = query[key];
-      }
-    });
-  } else {
-    _.each(filters, (converter, key) => {
-      const converted = converter(query[key], options);
-
-      if (converted !== undefined) {
-        object[key] = converted;
-      }
-    });
-  }
-
-  return object;
-}
-
-export const FILTERS: FilterSettings = {
-  $sort: (value: any) => convertSort(value),
-  $limit: (value: any, options: any) => getLimit(parse(value), options.paginate),
-  $skip: (value: any) => parse(value),
-  $select: (value: any) => value
-};
-
-export const OPERATORS = ['$in', '$nin', '$lt', '$lte', '$gt', '$gte', '$ne', '$or'];
-
-// Converts Feathers special query parameters and pagination settings
-// and returns them separately a `filters` and the rest of the query
-// as `query`
-export function filterQuery (query: Query, options: FilterQueryOptions = {}) {
-  const {
-    filters: additionalFilters = [],
-    operators: additionalOperators = []
-  } = options;
-  const baseFilters = assignFilters({}, query, FILTERS, options);
-  const filters = assignFilters(baseFilters, query, additionalFilters, options);
-
-  return {
-    filters,
-    query: cleanQuery(query, OPERATORS.concat(additionalOperators), filters) as Query
-  }
-}
diff --git a/packages/adapter-commons/src/index.ts b/packages/adapter-commons/src/index.ts
index 0b3777c516..4ce7f7b07a 100644
--- a/packages/adapter-commons/src/index.ts
+++ b/packages/adapter-commons/src/index.ts
@@ -3,7 +3,7 @@ import { Params } from '@feathersjs/feathers';
 
 export * from './declarations';
 export * from './service';
-export { filterQuery, FILTERS, OPERATORS } from './filter-query';
+export { filterQuery, FILTERS, OPERATORS } from './query';
 export * from './sort';
 
 // Return a function that filters a result object or array
diff --git a/packages/adapter-commons/src/query.ts b/packages/adapter-commons/src/query.ts
new file mode 100644
index 0000000000..757f8112ae
--- /dev/null
+++ b/packages/adapter-commons/src/query.ts
@@ -0,0 +1,138 @@
+import { _ } from '@feathersjs/commons';
+import { BadRequest } from '@feathersjs/errors';
+import { Query } from '@feathersjs/feathers';
+import { FilterQueryOptions, FilterSettings } from './declarations';
+
+const parse = (value: any) => typeof value !== 'undefined' ? parseInt(value, 10) : value;
+
+const isPlainObject = (value: any) => _.isObject(value) && value.constructor === {}.constructor;
+
+const validateQueryProperty = (query: any, operators: string[] = []): Query => {
+  if (!isPlainObject(query)) {
+    return query;
+  }
+
+  for (const key of Object.keys(query)) {
+    if (key.startsWith('$') && !operators.includes(key)) {
+      throw new BadRequest(`Invalid query parameter ${key}`, query);
+    }
+
+    const value = query[key];
+
+    if (isPlainObject(value)) {
+      query[key] = validateQueryProperty(value, operators);
+    }
+  }
+
+  return {
+    ...query
+  }
+}
+
+const getFilters = (query: Query, settings: FilterQueryOptions) => {
+  const filterNames = Object.keys(settings.filters);
+
+  return filterNames.reduce((current, key) => {
+    const queryValue = query[key];
+    const filter = settings.filters[key];
+
+    if (filter) {
+      const value = typeof filter === 'function' ? filter(queryValue, settings) : queryValue;
+
+      if (value !== undefined) {
+        current[key] = value;
+      }
+    }
+
+    return current;
+  }, {} as { [key: string]: any });
+}
+
+const getQuery = (query: Query, settings: FilterQueryOptions) => {
+  const keys = Object.keys(query).concat(Object.getOwnPropertySymbols(query) as any as string[]);
+
+  return keys.reduce((result, key) => {
+    if (typeof key === 'string' && key.startsWith('$')) {
+      if (settings.filters[key] === undefined) {
+        throw new BadRequest(`Invalid filter value ${key}`);
+      }
+    } else {
+      result[key] = validateQueryProperty(query[key], settings.operators);
+    }
+
+    return result;
+  }, {} as Query)
+}
+
+export const OPERATORS = ['$in', '$nin', '$lt', '$lte', '$gt', '$gte', '$ne', '$or'];
+
+export const FILTERS: FilterSettings = {
+  $skip: (value: any) => parse(value),
+  $sort: (sort: any): { [key: string]: number } => {
+    if (typeof sort !== 'object' || Array.isArray(sort)) {
+      return sort;
+    }
+
+    return Object.keys(sort).reduce((result, key) => {
+      result[key] = typeof sort[key] === 'object'
+        ? sort[key] : parse(sort[key]);
+
+      return result;
+    }, {} as { [key: string]: number });
+  },
+  $limit: (_limit: any, { paginate }: FilterQueryOptions) => {
+    const limit = parse(_limit);
+
+    if (paginate && (paginate.default || paginate.max)) {
+      const base = paginate.default || 0;
+      const lower = typeof limit === 'number' && !isNaN(limit) && limit >= 0 ? limit : base;
+      const upper = typeof paginate.max === 'number' ? paginate.max : Number.MAX_VALUE;
+
+      return Math.min(lower, upper);
+    }
+
+    return limit;
+  },
+  $select: (select: any) => {
+    if (Array.isArray(select)) {
+      return select.map(current => `${current}`);
+    }
+
+    return select;
+  },
+  $or: (or: any, { operators }: FilterQueryOptions) => {
+    if (Array.isArray(or)) {
+      return or.map(current => validateQueryProperty(current, operators));
+    }
+
+    return or;
+  }
+}
+
+/**
+ * Converts Feathers special query parameters and pagination settings
+ * and returns them separately as `filters` and the rest of the query
+ * as `query`. `options` also gets passed the pagination settings and
+ * a list of additional `operators` to allow when querying properties.
+ *
+ * @param query The initial query
+ * @param options Options for filtering the query
+ * @returns An object with `query` which contains the query without `filters`
+ * and `filters` which contains the converted values for each filter.
+ */
+export function filterQuery (_query: Query, options: FilterQueryOptions = {}) {
+  const query = _query || {};
+  const settings = {
+    ...options,
+    filters: {
+      ...FILTERS,
+      ...options.filters
+    },
+    operators: OPERATORS.concat(options.operators || [])
+  }
+
+  return {
+    filters: getFilters(query, settings),
+    query: getQuery(query, settings)
+  }
+}
diff --git a/packages/adapter-commons/src/service.ts b/packages/adapter-commons/src/service.ts
index 7ec607fbb6..68040cbdd6 100644
--- a/packages/adapter-commons/src/service.ts
+++ b/packages/adapter-commons/src/service.ts
@@ -1,15 +1,7 @@
-import { NotImplemented, BadRequest, MethodNotAllowed } from '@feathersjs/errors';
-import { ServiceMethods, Params, Id, NullableId, Paginated, Query } from '@feathersjs/feathers';
-import { AdapterParams, AdapterServiceOptions, FilterQueryOptions, PaginationOptions } from './declarations';
-import { filterQuery } from './filter-query';
-
-const callMethod = (self: any, name: any, ...args: any[]) => {
-  if (typeof self[name] !== 'function') {
-    return Promise.reject(new NotImplemented(`Method ${name} not available`));
-  }
-
-  return self[name](...args);
-};
+import { BadRequest, MethodNotAllowed } from '@feathersjs/errors';
+import { Id, NullableId, Paginated, Query } from '@feathersjs/feathers';
+import { AdapterParams, AdapterServiceOptions, InternalServiceMethods, PaginationOptions } from './declarations';
+import { filterQuery } from './query';
 
 const alwaysMulti: { [key: string]: boolean } = {
   find: true,
@@ -18,20 +10,27 @@ const alwaysMulti: { [key: string]: boolean } = {
 };
 
 /**
- * The base class that a database adapter can extend from.
+ * An abstract base class that a database adapter can extend from to implement the
+ * `__find`, `__get`, `__update`, `__patch` and `__remove` methods.
  */
-export class AdapterBase<O extends Partial<AdapterServiceOptions> = Partial<AdapterServiceOptions>> {
+export abstract class AdapterBase<
+  T = any,
+  D = Partial<T>,
+  P extends AdapterParams = AdapterParams,
+  O extends Partial<AdapterServiceOptions> = Partial<AdapterServiceOptions>
+  > implements InternalServiceMethods<T, D, P> {
   options: AdapterServiceOptions & O;
 
   constructor (options: O) {
-    this.options = Object.assign({
+    this.options = {
       id: 'id',
       events: [],
       paginate: false,
       multi: false,
-      filters: [],
-      allow: []
-    }, options);
+      filters: {},
+      operators: [],
+      ...options
+    };
   }
 
   get id () {
@@ -42,107 +41,231 @@ export class AdapterBase<O extends Partial<AdapterServiceOptions> = Partial<Adap
     return this.options.events;
   }
 
-  filterQuery (params: AdapterParams = {}, opts: FilterQueryOptions = {}) {
-    const paginate = typeof params.paginate !== 'undefined'
-      ? params.paginate
-      : this.getOptions(params).paginate;
-    const query: Query = { ...params.query };
-    const options = {
-      operators:  this.options.whitelist || this.options.allow || [],
-      filters: this.options.filters,
-      paginate,
-      ...opts
-    };
-    const result = filterQuery(query, options);
-
-    return {
-      paginate,
-      ...result
-    }
-  }
-
-  allowsMulti (method: string, params: AdapterParams = {}) {
+  /**
+   * Check if this adapter allows multiple updates for a method.
+   * @param method The method name to check.
+   * @param params The service call params.
+   * @returns Wether or not multiple updates are allowed.
+   */
+   allowsMulti (method: string, params: P = {} as P) {
     const always = alwaysMulti[method];
 
     if (typeof always !== 'undefined') {
       return always;
     }
 
-    const { multi: option } = this.getOptions(params);
+    const { multi } = this.getOptions(params);
 
-    if (option === true || option === false) {
-      return option;
+    if (multi === true || multi === false) {
+      return multi;
     }
 
-    return option.includes(method);
+    return multi.includes(method);
   }
 
-  getOptions (params: AdapterParams): AdapterServiceOptions & { model?: any } {
+  /**
+   * Returns the combined options for a service call. Options will be merged
+   * with `this.options` and `params.adapter` for dynamic overrides.
+   *
+   * @param params The parameters for the service method call
+   * @returns The actual options for this call
+   */
+  getOptions (params: P): AdapterServiceOptions {
+    const paginate = params.paginate !== undefined ? params.paginate : this.options.paginate;
+
     return {
       ...this.options,
+      paginate,
       ...params.adapter
     }
   }
-}
 
-export class AdapterService<
-  T = any,
-  D = Partial<T>,
-  O extends Partial<AdapterServiceOptions> = Partial<AdapterServiceOptions>,
-  P extends Params = AdapterParams
-> extends AdapterBase<O> implements ServiceMethods<T|Paginated<T>, D> {
-  find (params?: P & { paginate?: PaginationOptions }): Promise<Paginated<T>>;
-  find (params?: P & { paginate: false }): Promise<T[]>;
-  find (params?: P): Promise<T | T[] | Paginated<T>>;
-  find (params?: P): Promise<T[] | Paginated<T>> {
-    return callMethod(this, '_find', params);
+  /**
+   * Sanitize the incoming data, e.g. removing invalid keywords etc.
+   *
+   * @param data The data to sanitize
+   * @param _params Service call parameters
+   * @returns The sanitized data
+   */
+  async sanitizeData<X = Partial<D>> (data: X, _params: P) {
+    return data;
+  }
+
+  /**
+   * Returns a sanitized version of `params.query`, converting filter values
+   * (like $limit and $skip) into the expected type. Will throw an error if
+   * a `$` prefixed filter or operator value that is not allowed in `filters`
+   * or `operators` is encountered.
+   *
+   * @param params The service call parameter.
+   * @returns A new object containing the sanitized query.
+   */
+  async sanitizeQuery (params: P = {} as P): Promise<Query> {
+    const options = this.getOptions(params);
+    const { query, filters } = filterQuery(params.query, options)
+
+    return {
+      ...filters,
+      ...query
+    };
+  }
+
+  abstract $find(_params?: P & { paginate?: PaginationOptions }): Promise<Paginated<T>>;
+  abstract $find(_params?: P & { paginate: false }): Promise<T[]>;
+  abstract $find(params?: P): Promise<T[] | Paginated<T>>;
+
+  /**
+   * Retrieve all resources from this service, skipping any service-level hooks but sanitize the query
+   * with allowed filters and properties by calling `sanitizeQuery`.
+   *
+   * @param params - Service call parameters {@link Params}
+   * @see {@link HookLessServiceMethods}
+   * @see {@link https://docs.feathersjs.com/api/services.html#find-params|Feathers API Documentation: .find(params)}
+   */
+  async _find(_params?: P & { paginate?: PaginationOptions }): Promise<Paginated<T>>;
+  async _find(_params?: P & { paginate: false }): Promise<T[]>;
+  async _find(params?: P): Promise<T | T[] | Paginated<T>>;
+  async _find (params?: P): Promise<T | T[] | Paginated<T>> {
+    const query = await this.sanitizeQuery(params);
+
+    return this.$find({
+      ...params,
+      query
+    });
   }
 
-  get (id: Id, params?: P): Promise<T> {
-    return callMethod(this, '_get', id, params);
+  abstract $get(id: Id, params?: P): Promise<T>;
+
+  /**
+   * Retrieve a single resource matching the given ID, skipping any service-level hooks but sanitize the query
+   * with allowed filters and properties by calling `sanitizeQuery`.
+   *
+   * @param id - ID of the resource to locate
+   * @param params - Service call parameters {@link Params}
+   * @see {@link HookLessServiceMethods}
+   * @see {@link https://docs.feathersjs.com/api/services.html#get-id-params|Feathers API Documentation: .get(id, params)}
+   */
+  async _get (id: Id, params?: P): Promise<T> {
+    const query = await this.sanitizeQuery(params);
+
+    return this.$get(id, {
+      ...params,
+      query
+    });
   }
 
-  create (data: Partial<D>, params?: P): Promise<T>;
-  create (data: Partial<D>[], params?: P): Promise<T[]>;
-  create (data: Partial<D> | Partial<D>[], params?: P): Promise<T | T[]> {
+  abstract $create(data: Partial<D>, params?: P): Promise<T>;
+  abstract $create(data: Partial<D>[], params?: P): Promise<T[]>;
+  abstract $create(data: Partial<D> | Partial<D>[], params?: P): Promise<T | T[]>;
+
+  /**
+   * Create a new resource for this service, skipping any service-level hooks, sanitize the data
+   * and check if multiple updates are allowed.
+   *
+   * @param data - Data to insert into this service.
+   * @param params - Service call parameters {@link Params}
+   * @see {@link HookLessServiceMethods}
+   * @see {@link https://docs.feathersjs.com/api/services.html#create-data-params|Feathers API Documentation: .create(data, params)}
+   */
+  async _create(data: Partial<D>, params?: P): Promise<T>;
+  async _create(data: Partial<D>[], params?: P): Promise<T[]>;
+  async _create(data: Partial<D> | Partial<D>[], params?: P): Promise<T | T[]>;
+  async _create (data: Partial<D> | Partial<D>[], params?: P): Promise<T | T[]> {
     if (Array.isArray(data) && !this.allowsMulti('create', params)) {
-      return Promise.reject(new MethodNotAllowed('Can not create multiple entries'));
+      throw new MethodNotAllowed('Can not create multiple entries');
     }
 
-    return callMethod(this, '_create', data, params);
+    const payload = Array.isArray(data)
+      ? (await Promise.all(data.map(current => this.sanitizeData(current, params))))
+      : (await this.sanitizeData(data, params));
+
+    return this.$create(payload, params);
   }
 
-  update (id: Id, data: D, params?: P): Promise<T> {
+  abstract $update(id: Id, data: D, params?: P): Promise<T>;
+
+  /**
+   * Replace any resources matching the given ID with the given data, skipping any service-level hooks.
+   *
+   * @param id - ID of the resource to be updated
+   * @param data - Data to be put in place of the current resource.
+   * @param params - Service call parameters {@link Params}
+   * @see {@link HookLessServiceMethods}
+   * @see {@link https://docs.feathersjs.com/api/services.html#update-id-data-params|Feathers API Documentation: .update(id, data, params)}
+   */
+  async _update (id: Id, data: D, params?: P): Promise<T> {
     if (id === null || Array.isArray(data)) {
-      return Promise.reject(new BadRequest(
+      throw new BadRequest(
         'You can not replace multiple instances. Did you mean \'patch\'?'
-      ));
+      );
     }
 
-    return callMethod(this, '_update', id, data, params);
+    const payload = await this.sanitizeData(data, params);
+    const query = await this.sanitizeQuery(params);
+
+    return this.$update(id, payload, {
+      ...params,
+      query
+    });
   }
 
-  patch (id: Id, data: Partial<D>, params?: P): Promise<T>;
-  patch (id: null, data: Partial<D>, params?: P): Promise<T[]>;
-  patch (id: NullableId, data: Partial<D>, params?: P): Promise<T | T[]> {
+  abstract $patch(id: null, data: Partial<D>, params?: P): Promise<T[]>;
+  abstract $patch(id: Id, data: Partial<D>, params?: P): Promise<T>;
+  abstract $patch(id: NullableId, data: Partial<D>, params?: P): Promise<T | T[]>;
+
+  /**
+   * Merge any resources matching the given ID with the given data, skipping any service-level hooks.
+   * Sanitizes the query and data and checks it multiple updates are allowed.
+   *
+   * @param id - ID of the resource to be patched
+   * @param data - Data to merge with the current resource.
+   * @param params - Service call parameters {@link Params}
+   * @see {@link HookLessServiceMethods}
+   * @see {@link https://docs.feathersjs.com/api/services.html#patch-id-data-params|Feathers API Documentation: .patch(id, data, params)}
+   */
+  async _patch(id: null, data: Partial<D>, params?: P): Promise<T[]>;
+  async _patch(id: Id, data: Partial<D>, params?: P): Promise<T>;
+  async _patch(id: NullableId, data: Partial<D>, params?: P): Promise<T | T[]>;
+  async _patch (id: NullableId, data: Partial<D>, params?: P): Promise<T | T[]> {
     if (id === null && !this.allowsMulti('patch', params)) {
-      return Promise.reject(new MethodNotAllowed('Can not patch multiple entries'));
+      throw new MethodNotAllowed('Can not patch multiple entries');
     }
 
-    return callMethod(this, '_patch', id, data, params);
+    const query = await this.sanitizeQuery(params);
+    const payload = await this.sanitizeData(data, params);
+
+    return this.$patch(id, payload, {
+      ...params,
+      query
+    });
   }
 
-  remove (id: Id, params?: P): Promise<T>;
-  remove (id: null, params?: P): Promise<T[]>;
-  remove (id: NullableId, params?: P): Promise<T | T[]> {
+  abstract $remove(id: null, params?: P): Promise<T[]>;
+  abstract $remove(id: Id, params?: P): Promise<T>;
+  abstract $remove(id: NullableId, params?: P): Promise<T | T[]>;
+
+  /**
+   * Remove resources matching the given ID from the this service, skipping any service-level hooks.
+   * Sanitized the query and verifies that multiple updates are allowed.
+   *
+   * @param id - ID of the resource to be removed
+   * @param params - Service call parameters {@link Params}
+   * @see {@link HookLessServiceMethods}
+   * @see {@link https://docs.feathersjs.com/api/services.html#remove-id-params|Feathers API Documentation: .remove(id, params)}
+   */
+  async _remove(id: null, params?: P): Promise<T[]>;
+  async _remove(id: Id, params?: P): Promise<T>;
+  async _remove(id: NullableId, params?: P): Promise<T | T[]>;
+  async _remove (id: NullableId, params?: P): Promise<T | T[]> {
     if (id === null && !this.allowsMulti('remove', params)) {
-      return Promise.reject(new MethodNotAllowed('Can not remove multiple entries'));
+      throw new MethodNotAllowed('Can not remove multiple entries');
     }
 
-    return callMethod(this, '_remove', id, params);
-  }
+    const query = await this.sanitizeQuery(params);
 
-  async setup () {}
-
-  async teardown () {}
+    return this.$remove(id, {
+      ...params,
+      query
+    });
+  }
 }
diff --git a/packages/adapter-commons/test/fixture.ts b/packages/adapter-commons/test/fixture.ts
new file mode 100644
index 0000000000..efa639f212
--- /dev/null
+++ b/packages/adapter-commons/test/fixture.ts
@@ -0,0 +1,93 @@
+import { AdapterBase, AdapterParams, InternalServiceMethods, PaginationOptions } from '../src';
+import { Id, NullableId, Paginated, Query } from '@feathersjs/feathers';
+
+export type Data = {
+  id: Id
+}
+
+export class MethodBase extends AdapterBase<Data, Partial<Data>, AdapterParams> implements InternalServiceMethods<Data> {
+  async $find(_params?: AdapterParams & { paginate?: PaginationOptions }): Promise<Paginated<Data>>;
+  async $find(_params?: AdapterParams & { paginate: false }): Promise<Data[]>;
+  async $find(params?: AdapterParams): Promise<Data | Data[] | Paginated<Data>>;
+  async $find (params?: AdapterParams): Promise<Data | Data[] | Paginated<Data>> {
+    if (params && params.paginate === false) {
+      return {
+        total: 0,
+        limit: 10,
+        skip: 0,
+        data: []
+      }
+    }
+
+    return [];
+  }
+
+  async $get (id: Id, _params?: AdapterParams): Promise<Data> {
+    return { id };
+  }
+
+  async $create (data: Partial<Data>[], _params?: AdapterParams): Promise<Data[]>;
+  async $create (data: Partial<Data>, _params?: AdapterParams): Promise<Data>;
+  async $create (data: Partial<Data>|Partial<Data>[], _params?: AdapterParams): Promise<Data|Data[]> {
+    if (Array.isArray(data)) {
+      return [{
+        id: 'something'
+      }];
+    }
+
+    return {
+      id: 'something',
+      ...data
+    }
+  }
+
+  async create (data: Partial<Data>|Partial<Data>[], params?: AdapterParams): Promise<Data|Data[]> {
+    return this._create(data, params);
+  }
+
+  async $update (id: NullableId, _data: Data, _params?: AdapterParams) {
+    return Promise.resolve({ id });
+  }
+
+  async $patch (id: null, _data: Partial<Data>, _params?: AdapterParams): Promise<Data[]>;
+  async $patch (id: Id, _data: Partial<Data>, _params?: AdapterParams): Promise<Data>;
+  async $patch (id: NullableId, _data: Partial<Data>, _params?: AdapterParams): Promise<Data|Data[]> {
+    if (id === null) {
+      return []
+    }
+
+    return { id };
+  }
+
+  async $remove (id: null, _params?: AdapterParams): Promise<Data[]>;
+  async $remove (id: Id, _params?: AdapterParams): Promise<Data>;
+  async $remove (id: NullableId, _params?: AdapterParams) {
+    if (id === null) {
+      return [] as Data[];
+    }
+
+    return { id };
+  }
+}
+
+export class MethodService extends MethodBase {
+  find (params?: AdapterParams<Query, any>): Promise<Data|Data[]|Paginated<Data>> {
+    return this._find(params);
+  }
+
+  get (id: Id, params?: AdapterParams): Promise<Data> {
+    return this._get(id, params);
+  }
+
+  async update (id: Id, data: Data, params?: AdapterParams) {
+    return this._update(id, data, params);
+  }
+
+  async patch (id: NullableId, data: Partial<Data>, params?: AdapterParams) {
+    return this._patch(id, data, params);
+  }
+
+  async remove (id: NullableId, params?: AdapterParams) {
+    return this._remove(id, params);
+  }
+}
diff --git a/packages/adapter-commons/test/filter-query.test.ts b/packages/adapter-commons/test/query.test.ts
similarity index 89%
rename from packages/adapter-commons/test/filter-query.test.ts
rename to packages/adapter-commons/test/query.test.ts
index b295c9c197..55ff60c226 100644
--- a/packages/adapter-commons/test/filter-query.test.ts
+++ b/packages/adapter-commons/test/query.test.ts
@@ -44,7 +44,7 @@ describe('@feathersjs/adapter-commons/filterQuery', () => {
         assert.ok(false, 'Should never get here');
       } catch (error: any) {
         assert.strictEqual(error.name, 'BadRequest');
-        assert.strictEqual(error.message, 'Invalid query parameter $foo');
+        assert.strictEqual(error.message, 'Invalid filter value $foo');
       }
     });
 
@@ -96,16 +96,16 @@ describe('@feathersjs/adapter-commons/filterQuery', () => {
     describe('pagination', () => {
       it('limits with default pagination', () => {
         const { filters } = filterQuery({}, { paginate: { default: 10 } });
+        const { filters: filtersNeg } = filterQuery({ $limit: -20 }, { paginate: { default: 5, max: 10 } });
 
         assert.strictEqual(filters.$limit, 10);
+        assert.strictEqual(filtersNeg.$limit, 5);
       });
 
       it('limits with max pagination', () => {
         const { filters } = filterQuery({ $limit: 20 }, { paginate: { default: 5, max: 10 } });
-        const { filters: filtersNeg } = filterQuery({ $limit: -20 }, { paginate: { default: 5, max: 10 } });
 
         assert.strictEqual(filters.$limit, 10);
-        assert.strictEqual(filtersNeg.$limit, 10);
       });
 
       it('limits with default pagination when not a number', () => {
@@ -223,6 +223,16 @@ describe('@feathersjs/adapter-commons/filterQuery', () => {
         message: 'Invalid query parameter $exists'
       });
     });
+
+    it('allows default operators in $or', () => {
+      const { filters } = filterQuery({
+        $or: [{ value: { $gte: 10 } }]
+      });
+
+      assert.deepStrictEqual(filters, {
+        $or: [{ value: { $gte: 10 } }]
+      });
+    });
   });
 
   describe('additional filters', () => {
@@ -231,13 +241,18 @@ describe('@feathersjs/adapter-commons/filterQuery', () => {
         filterQuery({ $select: 1, $known: 1 });
         assert.ok(false, 'Should never get here');
       } catch (error: any) {
-        assert.strictEqual(error.message, 'Invalid query parameter $known');
+        assert.strictEqual(error.message, 'Invalid filter value $known');
       }
     });
 
     it('returns default and known additional filters (array)', () => {
       const query = { $select: ['a', 'b'], $known: 1, $unknown: 1 };
-      const { filters } = filterQuery(query, { filters: [ '$known', '$unknown' ] });
+      const { filters } = filterQuery(query, {
+        filters: {
+          $known: true,
+          $unknown: true
+        }
+      });
 
       assert.strictEqual(filters.$unknown, 1);
       assert.strictEqual(filters.$known, 1);
@@ -259,12 +274,18 @@ describe('@feathersjs/adapter-commons/filterQuery', () => {
   describe('additional operators', () => {
     it('returns query with default and known additional operators', () => {
       const { query } = filterQuery({
-        $ne: 1, $known: 1
+        prop: { $ne: 1, $known: 1 }
       }, { operators: [ '$known' ] });
 
-      assert.strictEqual(query.$ne, 1);
-      assert.strictEqual(query.$known, 1);
-      assert.strictEqual(query.$unknown, undefined);
+      assert.deepStrictEqual(query, { prop: { '$ne': 1, '$known': 1 } });
+    });
+
+    it('throws an error with unknown query operator', () => {
+      assert.throws(() => filterQuery({
+        prop: { $unknown: 'something' }
+      }), {
+        message: 'Invalid query parameter $unknown'
+      });
     });
   });
 });
diff --git a/packages/adapter-commons/test/service.test.ts b/packages/adapter-commons/test/service.test.ts
index 2f5635a970..59db52e530 100644
--- a/packages/adapter-commons/test/service.test.ts
+++ b/packages/adapter-commons/test/service.test.ts
@@ -1,97 +1,11 @@
 /* eslint-disable @typescript-eslint/no-unused-vars */
 import assert from 'assert';
-import { NotImplemented } from '@feathersjs/errors';
-import { AdapterService, InternalServiceMethods, PaginationOptions } from '../src';
-import { Id, NullableId, Paginated } from '@feathersjs/feathers';
-import { AdapterParams } from '../lib';
+import { MethodService } from './fixture';
 
 const METHODS: [ 'find', 'get', 'create', 'update', 'patch', 'remove' ] = [ 'find', 'get', 'create', 'update', 'patch', 'remove' ];
 
 describe('@feathersjs/adapter-commons/service', () => {
-  class CustomService extends AdapterService {
-  }
-
-  describe('errors when method does not exit', () => {
-    METHODS.forEach(method => {
-      it(`${method}`, () => {
-        const service = new CustomService({});
-
-        // @ts-ignore
-        return service[method]().then(() => {
-          throw new Error('Should never get here');
-        }).catch((error: Error) => {
-          assert.ok(error instanceof NotImplemented);
-          assert.strictEqual(error.message, `Method _${method} not available`);
-        });
-      });
-    });
-  });
-
   describe('works when methods exist', () => {
-    type Data = {
-      id: Id
-    }
-
-    class MethodService extends AdapterService<Data> implements InternalServiceMethods<Data> {
-      _find (_params?: AdapterParams & { paginate?: PaginationOptions }): Promise<Paginated<Data>>;
-      _find (_params?: AdapterParams & { paginate: false }): Promise<Data[]>;
-      async _find (params?: AdapterParams): Promise<Paginated<Data>|Data[]> {
-        if (params && params.paginate === false) {
-          return {
-            total: 0,
-            limit: 10,
-            skip: 0,
-            data: []
-          }
-        }
-
-        return [];
-      }
-
-      async _get (id: Id, _params?: AdapterParams) {
-        return { id };
-      }
-
-      async _create (data: Partial<Data>[], _params?: AdapterParams): Promise<Data[]>;
-      async _create (data: Partial<Data>, _params?: AdapterParams): Promise<Data>;
-      async _create (data: Partial<Data>|Partial<Data>[], _params?: AdapterParams): Promise<Data|Data[]> {
-        if (Array.isArray(data)) {
-          return [{
-            id: 'something'
-          }];
-        }
-
-        return {
-          id: 'something',
-          ...data
-        }
-      }
-
-      async _update (id: NullableId, _data: any, _params?: AdapterParams) {
-        return Promise.resolve({ id });
-      }
-
-      async _patch (id: null, _data: any, _params?: AdapterParams): Promise<Data[]>;
-      async _patch (id: Id, _data: any, _params?: AdapterParams): Promise<Data>;
-      async _patch (id: NullableId, _data: any, _params?: AdapterParams): Promise<Data|Data[]> {
-        if (id === null) {
-          return []
-        }
-
-        return { id };
-      }
-
-      async _remove (id: null, _params?: AdapterParams): Promise<Data[]>;
-      async _remove (id: Id, _params?: AdapterParams): Promise<Data>;
-      async _remove (id: NullableId, _params?: AdapterParams) {
-        if (id === null) {
-          return [] as Data[];
-        }
-
-        return { id };
-      }
-    }
-
     METHODS.forEach(method => {
       it(`${method}`, () => {
         const service = new MethodService({});
@@ -110,77 +24,82 @@ describe('@feathersjs/adapter-commons/service', () => {
       });
     });
 
-    it('does not allow multi patch', () => {
+    it('does not allow multi patch', async () => {
       const service = new MethodService({});
 
-      return service.patch(null, {})
-        .then(() => assert.ok(false))
-        .catch(error => {
-          assert.strictEqual(error.name, 'MethodNotAllowed');
-          assert.strictEqual(error.message, 'Can not patch multiple entries');
-        });
+      await assert.rejects(() => service.patch(null, {}), {
+        name: 'MethodNotAllowed',
+        message: 'Can not patch multiple entries'
+      });
     });
 
-    it('does not allow multi remove', () => {
+    it('does not allow multi remove', async () => {
       const service = new MethodService({});
 
-      return service.remove(null, {})
-        .then(() => assert.ok(false))
-        .catch(error => {
-          assert.strictEqual(error.name, 'MethodNotAllowed');
-          assert.strictEqual(error.message, 'Can not remove multiple entries');
-        });
+      await assert.rejects(() => service.remove(null, {}), {
+        name: 'MethodNotAllowed',
+        message: 'Can not remove multiple entries'
+      });
     });
 
-    it('does not allow multi create', () => {
+    it('does not allow multi create', async () => {
       const service = new MethodService({});
 
-      return service.create([])
-        .then(() => assert.ok(false))
-        .catch(error => {
-          assert.strictEqual(error.name, 'MethodNotAllowed');
-          assert.strictEqual(error.message, 'Can not create multiple entries');
-        });
+      await assert.rejects(() => service.create([], {}), {
+        name: 'MethodNotAllowed',
+        message: 'Can not create multiple entries'
+      });
     });
 
-    it('multi can be set to true', () => {
+    it('multi can be set to true', async () => {
       const service = new MethodService({});
 
       service.options.multi = true;
 
-      return service.create([])
-        .then(() => assert.ok(true));
+      await service.create([]);
     });
   });
 
-  it('filterQuery', () => {
-    const service = new CustomService({
-      allow: [ '$something' ]
-    });
-    const filtered = service.filterQuery({
-      query: { $limit: 10, test: 'me' }
+  it('sanitizeQuery', async () => {
+    const service = new MethodService({
+      filters: {
+        $something: true
+      },
+      operators: [ '$test' ]
     });
 
-    assert.deepStrictEqual(filtered, {
-      paginate: false,
-      filters: { $limit: 10 },
-      query: { test: 'me' }
-    });
+    assert.deepStrictEqual(await service.sanitizeQuery({
+      query: { $limit: '10', test: 'me' }
+    }), { $limit: 10, test: 'me' });
 
-    const withAllowed = service.filterQuery({
-      query: { $limit: 10, $something: 'else' }
-    });
+    assert.deepStrictEqual(await service.sanitizeQuery({
+      adapter: {
+        paginate: { max: 2 }
+      },
+      query: { $limit: '10', test: 'me' }
+    }), { $limit: 2, test: 'me' });
 
-    assert.deepStrictEqual(withAllowed, {
-      paginate: false,
-      filters: { $limit: 10 },
-      query: { $something: 'else' }
+    await assert.rejects(() => service.sanitizeQuery({
+      query: { name: { $bla: 'me' } }
+    }), {
+      message: 'Invalid query parameter $bla'
     });
+
+    assert.deepStrictEqual(await service.sanitizeQuery({
+      adapter: {
+        operators: ['$bla']
+      },
+      query: { name: { $bla: 'Dave' } }
+    }), { name: { $bla: 'Dave' } });
   });
 
   it('getOptions', () => {
-    const service = new AdapterService({
-      multi: true
+    const service = new MethodService({
+      multi: true,
+      paginate: {
+        default: 1,
+        max: 10
+      }
     });
     const opts = service.getOptions({
       adapter: {
@@ -197,20 +116,33 @@ describe('@feathersjs/adapter-commons/service', () => {
       events: [],
       paginate: { default: 10, max: 100 },
       multi: [ 'create' ],
-      filters: [],
-      allow: []
+      filters: {},
+      operators: []
+    });
+
+    const notPaginated = service.getOptions({
+      paginate: false
+    });
+
+    assert.deepStrictEqual(notPaginated, {
+      id: 'id',
+      events: [],
+      paginate: false,
+      multi: true,
+      filters: {},
+      operators: []
     });
   });
 
   it('allowsMulti', () => {
     context('with true', () => {
-      const service = new AdapterService({multi: true});
+      const service = new MethodService({multi: true});
 
-      it('does return true for multible methodes', () => {
+      it('does return true for multiple methodes', () => {
         assert.equal(service.allowsMulti('patch'), true);
       });
 
-      it('does return false for always non-multible methodes', () => {
+      it('does return false for always non-multiple methodes', () => {
         assert.equal(service.allowsMulti('update'), false);
       });
 
@@ -220,13 +152,13 @@ describe('@feathersjs/adapter-commons/service', () => {
     });
 
     context('with false', () => {
-      const service = new AdapterService({multi: false});
+      const service = new MethodService({multi: false});
 
-      it('does return false for multible methodes', () => {
+      it('does return false for multiple methodes', () => {
         assert.equal(service.allowsMulti('remove'), false);
       });
 
-      it('does return true for always multible methodes', () => {
+      it('does return true for always multiple methodes', () => {
         assert.equal(service.allowsMulti('find'), true);
       });
 
@@ -236,17 +168,17 @@ describe('@feathersjs/adapter-commons/service', () => {
     });
 
     context('with array', () => {
-      const service = new AdapterService({multi: ['create', 'get', 'other']});
+      const service = new MethodService({multi: ['create', 'get', 'other']});
 
-      it('does return true for specified multible methodes', () => {
+      it('does return true for specified multiple methodes', () => {
         assert.equal(service.allowsMulti('create'), true);
       });
 
-      it('does return false for non-specified multible methodes', () => {
+      it('does return false for non-specified multiple methodes', () => {
         assert.equal(service.allowsMulti('patch'), false);
       });
 
-      it('does return false for specified always multible methodes', () => {
+      it('does return false for specified always multiple methodes', () => {
         assert.equal(service.allowsMulti('get'), false);
       });
 
diff --git a/packages/adapter-tests/src/basic.ts b/packages/adapter-tests/src/basic.ts
index a9599a3a72..26586e473c 100644
--- a/packages/adapter-tests/src/basic.ts
+++ b/packages/adapter-tests/src/basic.ts
@@ -49,6 +49,30 @@ export default (test: AdapterBasicTest, app: any, _errors: any, serviceName: str
       test('._remove', () => {
         assert.strictEqual(typeof service._remove, 'function');
       });
+
+      test('.$get', () => {
+        assert.strictEqual(typeof service.$get, 'function');
+      });
+
+      test('.$find', () => {
+        assert.strictEqual(typeof service.$find, 'function');
+      });
+
+      test('.$create', () => {
+        assert.strictEqual(typeof service.$create, 'function');
+      });
+
+      test('.$update', () => {
+        assert.strictEqual(typeof service.$update, 'function');
+      });
+
+      test('.$patch', () => {
+        assert.strictEqual(typeof service.$patch, 'function');
+      });
+
+      test('.$remove', () => {
+        assert.strictEqual(typeof service.$remove, 'function');
+      });
     });
   });
 };
diff --git a/packages/adapter-tests/src/declarations.ts b/packages/adapter-tests/src/declarations.ts
index 80bb142385..fa76d68767 100644
--- a/packages/adapter-tests/src/declarations.ts
+++ b/packages/adapter-tests/src/declarations.ts
@@ -15,7 +15,13 @@ export type AdapterBasicTestName =
   '._create' |
   '._update' |
   '._patch' |
-  '._remove';
+  '._remove'|
+  '.$get' |
+  '.$find' |
+  '.$create' |
+  '.$update' |
+  '.$patch' |
+  '.$remove';
 
 export type AdapterMethodsTestName =
   '.get' |
diff --git a/packages/adapter-tests/src/index.ts b/packages/adapter-tests/src/index.ts
index 51b3d31bd4..2757a9710e 100644
--- a/packages/adapter-tests/src/index.ts
+++ b/packages/adapter-tests/src/index.ts
@@ -28,7 +28,6 @@ const adapterTests = (testNames: AdapterTestName[]) => {
 
     describe(`Adapter tests for '${serviceName}' service with '${idProp}' id property`, () => {
       after(() => {
-        console.log('\n');
         testNames.forEach(name => {
           if (!allTests.includes(name)) {
             console.error(`WARNING: '${name}' test is not part of the test suite`);
diff --git a/packages/adapter-tests/test/index.test.ts b/packages/adapter-tests/test/index.test.ts
index 445c3d9d6c..d825301296 100644
--- a/packages/adapter-tests/test/index.test.ts
+++ b/packages/adapter-tests/test/index.test.ts
@@ -9,6 +9,12 @@ const testSuite = adapterTests([
   '._update',
   '._patch',
   '._remove',
+  '.$get',
+  '.$find',
+  '.$create',
+  '.$update',
+  '.$patch',
+  '.$remove',
   '.get',
   '.get + $select',
   '.get + id + query',
diff --git a/packages/memory/src/index.ts b/packages/memory/src/index.ts
index a05ea48b82..178f0d9963 100644
--- a/packages/memory/src/index.ts
+++ b/packages/memory/src/index.ts
@@ -1,6 +1,6 @@
-import { MethodNotAllowed, NotFound } from '@feathersjs/errors';
+import { NotFound } from '@feathersjs/errors';
 import { _ } from '@feathersjs/commons';
-import { sorter, select, AdapterBase, AdapterServiceOptions, InternalServiceMethods, PaginationOptions } from '@feathersjs/adapter-commons';
+import { sorter, select, AdapterBase, AdapterServiceOptions, PaginationOptions } from '@feathersjs/adapter-commons';
 import sift from 'sift';
 import { NullableId, Id, Params, ServiceMethods, Paginated } from '@feathersjs/feathers';
 
@@ -21,8 +21,7 @@ const _select = (data: any, params: any, ...args: any[]) => {
   return base(JSON.parse(JSON.stringify(data)));
 };
 
-export class MemoryAdapter<T = any, D = Partial<T>, P extends Params = Params> extends AdapterBase<MemoryServiceOptions<T>>
-    implements InternalServiceMethods<T, D, P> {
+export class MemoryAdapter<T = any, D = Partial<T>, P extends Params = Params> extends AdapterBase<T, D, P, MemoryServiceOptions<T>> {
   store: MemoryServiceStore<T>;
   _uId: number;
 
@@ -41,21 +40,29 @@ export class MemoryAdapter<T = any, D = Partial<T>, P extends Params = Params> e
 
   async getEntries (_params?: P) {
     const params = _params || {} as P;
-    const { query } = this.filterQuery(params);
 
-    return this._find({
+    return this.$find({
       ...params,
-      paginate: false,
-      query
+      paginate: false
     });
   }
 
-  async _find (_params?: P & { paginate?: PaginationOptions }): Promise<Paginated<T>>;
-  async _find (_params?: P & { paginate: false }): Promise<T[]>;
-  async _find (_params?: P): Promise<Paginated<T>|T[]>;
-  async _find (_params?: P): Promise<Paginated<T>|T[]> {
-    const params = _params || {} as P;
-    const { query, filters, paginate } = this.filterQuery(params);
+  getQuery (params: P) {
+    const { $skip, $sort, $limit, $select, ...query } = params.query || {};
+
+    return {
+      query,
+      filters: { $skip, $sort, $limit, $select }
+    }
+  }
+
+  async $find (_params?: P & { paginate?: PaginationOptions }): Promise<Paginated<T>>;
+  async $find (_params?: P & { paginate: false }): Promise<T[]>;
+  async $find (_params?: P): Promise<Paginated<T>|T[]>;
+  async $find (params: P = {} as P): Promise<Paginated<T>|T[]> {
+    const { paginate } = this.getOptions(params);
+    const { query, filters } = this.getQuery(params);
+
     let values = _.values(this.store).filter(this.options.matcher(query));
     const total = values.length;
 
@@ -78,18 +85,17 @@ export class MemoryAdapter<T = any, D = Partial<T>, P extends Params = Params> e
       data: values.map(value => _select(value, params))
     };
 
-    if (!(paginate && paginate.default)) {
+    if (!paginate) {
       return result.data;
     }
 
     return result;
   }
 
-  async _get (id: Id, _params?: P): Promise<T> {
-    const params = _params || {} as P;
+  async $get (id: Id, params: P = {} as P): Promise<T> {
+    const { query } = this.getQuery(params);
 
     if (id in this.store) {
-      const { query } = this.filterQuery(params);
       const value = this.store[id];
 
       if (this.options.matcher(query)(value)) {
@@ -100,19 +106,12 @@ export class MemoryAdapter<T = any, D = Partial<T>, P extends Params = Params> e
     throw new NotFound(`No record found for id '${id}'`);
   }
 
-  // Create without hooks and mixins that can be used internally
-  async _create (data: Partial<D>, params?: P): Promise<T>;
-  async _create (data: Partial<D>[], params?: P): Promise<T[]>;
-  async _create (data: Partial<D>|Partial<D>[], _params?: P): Promise<T|T[]>;
-  async _create (data: Partial<D>|Partial<D>[], _params?: P): Promise<T|T[]> {
-    const params = _params || {} as P;
-
+  async $create (data: Partial<D>, params?: P): Promise<T>;
+  async $create (data: Partial<D>[], params?: P): Promise<T[]>;
+  async $create (data: Partial<D>|Partial<D>[], _params?: P): Promise<T|T[]>;
+  async $create (data: Partial<D>|Partial<D>[], params: P = {} as P): Promise<T|T[]> {
     if (Array.isArray(data)) {
-      if (!this.allowsMulti('create', params)) {
-        throw new MethodNotAllowed('Can not create multiple entries');
-      }
-
-      return Promise.all(data.map(current => this._create(current, params)));
+      return Promise.all(data.map(current => this.$create(current, params)));
     }
 
     const id = (data as any)[this.id] || this._uId++;
@@ -122,12 +121,8 @@ export class MemoryAdapter<T = any, D = Partial<T>, P extends Params = Params> e
     return _select(result, params, this.id) as T;
   }
 
-  async _update (id: Id, data: D, params: P = {} as P): Promise<T> {
-    if (id === null || Array.isArray(data)) {
-      throw new MethodNotAllowed('You can not replace multiple instances. Did you mean \'patch\'?');
-    }
-
-    const oldEntry = await this._get(id);
+  async $update (id: Id, data: D, params: P = {} as P): Promise<T> {
+    const oldEntry = await this.$get(id);
     // We don't want our id to change type if it can be coerced
     const oldId = (oldEntry as any)[this.id];
 
@@ -136,14 +131,14 @@ export class MemoryAdapter<T = any, D = Partial<T>, P extends Params = Params> e
 
     this.store[id] = _.extend({}, data, { [this.id]: id });
 
-    return this._get(id, params);
+    return this.$get(id, params);
   }
 
-  async _patch (id: null, data: Partial<D>, params?: P): Promise<T[]>;
-  async _patch (id: Id, data: Partial<D>, params?: P): Promise<T>;
-  async _patch (id: NullableId, data: Partial<D>, _params?: P): Promise<T|T[]>;
-  async _patch (id: NullableId, data: Partial<D>, _params?: P): Promise<T|T[]> {
-    const params = _params || {} as P;
+  async $patch (id: null, data: Partial<D>, params?: P): Promise<T[]>;
+  async $patch (id: Id, data: Partial<D>, params?: P): Promise<T>;
+  async $patch (id: NullableId, data: Partial<D>, _params?: P): Promise<T|T[]>;
+  async $patch (id: NullableId, data: Partial<D>, params: P = {} as P): Promise<T|T[]> {
+    const { query } = this.getQuery(params);
     const patchEntry = (entry: T) => {
       const currentId = (entry as any)[this.id];
 
@@ -153,38 +148,35 @@ export class MemoryAdapter<T = any, D = Partial<T>, P extends Params = Params> e
     };
 
     if (id === null) {
-      if(!this.allowsMulti('patch', params)) {
-        throw new MethodNotAllowed('Can not patch multiple entries');
-      }
-
-      const entries = await this.getEntries(params);
+      const entries = await this.getEntries({
+        ...params,
+        query
+      });
 
       return entries.map(patchEntry);
     }
 
-    return patchEntry(await this._get(id, params)); // Will throw an error if not found
+    return patchEntry(await this.$get(id, params)); // Will throw an error if not found
   }
 
-  // Remove without hooks and mixins that can be used internally
-  async _remove (id: null, params?: P): Promise<T[]>;
-  async _remove (id: Id, params?: P): Promise<T>;
-  async _remove (id: NullableId, _params?: P): Promise<T|T[]>;
-  async _remove (id: NullableId, _params?: P): Promise<T|T[]>  {
-    const params = _params || {} as P;
+  async $remove (id: null, params?: P): Promise<T[]>;
+  async $remove (id: Id, params?: P): Promise<T>;
+  async $remove (id: NullableId, _params?: P): Promise<T|T[]>;
+  async $remove (id: NullableId, params: P = {} as P): Promise<T|T[]>  {
+    const { query } = this.getQuery(params);
 
     if (id === null) {
-      if(!this.allowsMulti('remove', params)) {
-        throw new MethodNotAllowed('Can not remove multiple entries');
-      }
+      const entries = await this.getEntries({
+        ...params,
+        query
+      });
 
-      const entries = await this.getEntries(params);
-
-      return Promise.all(entries.map(current =>
-        this._remove((current as any)[this.id] as Id, params)
+      return Promise.all(entries.map((current: any) =>
+        this.$remove(current[this.id] as Id, params)
       ));
     }
 
-    const entry = await this._get(id, params);
+    const entry = await this.$get(id, params);
 
     delete this.store[id];
 
@@ -198,7 +190,7 @@ export class MemoryService<T = any, D = Partial<T>, P extends Params = Params>
   async find (params?: P & { paginate: false }): Promise<T[]>;
   async find (params?: P): Promise<Paginated<T>|T[]>;
   async find (params?: P): Promise<Paginated<T>|T[]> {
-    return this._find(params)
+    return this._find(params) as any;
   }
 
   async get (id: Id, params?: P): Promise<T> {
@@ -232,4 +224,4 @@ export function memory<T = any, D = Partial<T>, P extends Params = Params> (
   options: Partial<MemoryServiceOptions<T>> = {}
 ) {
   return new MemoryService<T, D, P>(options)
-}
\ No newline at end of file
+}
diff --git a/packages/memory/test/index.test.ts b/packages/memory/test/index.test.ts
index 386bc883de..dbaccce017 100644
--- a/packages/memory/test/index.test.ts
+++ b/packages/memory/test/index.test.ts
@@ -14,6 +14,12 @@ const testSuite = adapterTests([
   '._update',
   '._patch',
   '._remove',
+  '.$get',
+  '.$find',
+  '.$create',
+  '.$update',
+  '.$patch',
+  '.$remove',
   '.get',
   '.get + $select',
   '.get + id + query',