Return result set from function with dynamic query

I am trying to return result set from function which use executes a command string with Execute statement.

create function GetLogApiCalls
(
@displayStart int,
@displayLength int,
@searchString nvarchar(1000),
@orderBy nvarchar(100),
@orderByDirection varchar(50)
)
returns @logs table(
[Id]                INT     NOT NULL,
[Provider]          NVARCHAR (10)    NULL,
[RequestIdentifier] UNIQUEIDENTIFIER NULL,
[RequestData]       NVARCHAR (MAX)   NULL,
[ResponseData]      NVARCHAR (MAX)   NULL,
[UserName]          NVARCHAR (50)    NULL,
[AccountName]       NVARCHAR (100)   NULL,
[AccountId]         INT              NULL,
[CreatedUserId]     INT              NULL,
[CreatedDate]       DATETIME         NULL,
[MethodName]        NVARCHAR (100)   NULL
)
AS
BEGIN
   
DECLARE @sqlScript NVARCHAR(1000)

IF(@searchString IS NOT NULL AND LEN(@searchString) > 0)
BEGIN
SET @sqlScript = N'SELECT * FROM 
                (
                 SELECT ROW_NUMBER() OVER (ORDER BY '+@orderBy+' '+@orderByDirection+') AS rn , * from LogAPICall where Provider like ''%'+@searchString+'%'' or MethodName like ''%'+@searchString+'%''
                ) as result where result.rn between '+ CONVERT(varchar,@displayStart) + ' and ' + CONVERT(varchar,@displayLength)
END
ELSE
BEGIN
SET @sqlScript = N'SELECT * FROM 
                (
                 SELECT ROW_NUMBER() OVER (ORDER BY '+@orderBy+' '+@orderByDirection+') AS RN , * FROM LogAPICall
                ) AS result WHERE result.rn between ' + CONVERT(varchar,@displayStart) + ' and ' + CONVERT(varchar,@displayLength)
END

 insert @logs
 Execute(@sqlScript)
return
END

When I run this code, it returns following error :

Invalid use of a side-effecting operator 'INSERT EXEC' within a function.

Why?


Sorry, you cannot perform dynamic SQL in a function, period. You can re-write your function to not require dynamic SQL, but does that really solve the underlying problem? What will you do when you need a procedure and are still hand-cuffed by this completely unrelated Entity Framework issue involving return type? Clearly something is broken with the way you have configured EF or hooked up your procedure, otherwise a lot of people would be complaining that EF doesn't work with procedures that return result sets. Do you think that's likely?

CREATE FUNCTION dbo.GetLogApiCalls -- dbo prefix, always
(
  @displayStart     INT,
  @displayLength    INT, -- is this a page size, like 20, or @displayEnd?
  @searchString     NVARCHAR(1000),
  @orderBy          NVARCHAR(100),
  @orderByDirection VARCHAR(4)
)
RETURNS TABLE
AS -- make it an inline TVF. Multi-statement TVFs can be a perf nightmare.
RETURN 
(
  SELECT * FROM 
  (
    SELECT rn = ROW_NUMBER() OVER (ORDER BY 
     CASE @orderByDirection WHEN 'ASC' THEN
      CASE @orderBy WHEN N'Provider'     THEN Provider
                    WHEN N'RequestData'  THEN RequestData
                    WHEN N'ResponseData' THEN ResponseData
                    WHEN N'UserName'     THEN UserName
                    WHEN N'AccountName'  THEN AccountName
                    WHEN N'MethodName'   THEN MethodName
                    WHEN N'RequestIdentifier'
                                         THEN CONVERT(CHAR(36), RequestIdentifier)
                    WHEN N'CreatedDate'  THEN CONVERT(CHAR(23), CreatedDate, 126)
      END
     END,
     CASE @orderByDirection WHEN 'ASC' THEN
      CASE @orderBy WHEN N'Id'            THEN Id
                    WHEN N'AccountId'     THEN AccountId
                    WHEN N'CreatedUserId' THEN CreatedUserId
      END
     END,
     CASE @orderByDirection WHEN 'DESC' THEN
      CASE @orderBy WHEN N'Provider'     THEN Provider
                    WHEN N'RequestData'  THEN RequestData
                    WHEN N'ResponseData' THEN ResponseData
                    WHEN N'UserName'     THEN UserName
                    WHEN N'AccountName'  THEN AccountName
                    WHEN N'MethodName'   THEN MethodName
                    WHEN N'RequestIdentifier' 
                                         THEN CONVERT(CHAR(36), RequestIdentifier)
                    WHEN N'CreatedDate'  THEN CONVERT(CHAR(23), CreatedDate, 126)
      END
     END DESC,
     CASE @orderByDirection WHEN 'DESC' THEN
      CASE @orderBy WHEN N'Id'            THEN Id
                    WHEN N'AccountId'     THEN AccountId
                    WHEN N'CreatedUserId' THEN CreatedUserId
      END
     END DESC), [Id],[Provider],[RequestIdentifier],[RequestData],
     [ResponseData],[UserName],[AccountName],[AccountId],
     [CreatedUserId],[CreatedDate],[MethodName]
  FROM dbo.LogAPICall
  WHERE LEN(@searchString) = 0 OR 
  (
    @searchString > ''  AND 
    (   
         Provider   LIKE '%' + @searchString + '%'
      OR MethodName LIKE '%' + @searchString + '%'
    )
  )
) AS x 
WHERE rn BETWEEN @displayStart AND @displayStart + @displayLength - 1 -- assumption
);

This dynamic ORDER BY is the devil, though. Even as an inline TVF this will only perform well for different parameters by taking a hit and adding OPTION (RECOMPILE) in any referencing query. Solution? Use a stored procedure with dynamic SQL, and figure out your Entity Framework configuration issue separately. As a stored procedure this would be so much better as:

CREATE PROCEDURE dbo.GetLogApiCalls -- dbo prefix, always
  @displayStart     INT,
  @displayLength    INT, -- is this a page size, like 20, or @displayEnd?
  @searchString     NVARCHAR(1000),
  @orderBy          NVARCHAR(100),
  @orderByDirection VARCHAR(4)
AS
BEGIN
  SET NOCOUNT ON;

  DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM (
    SELECT rn = ROW_NUMBER() OVER (ORDER BY ' 
        + @orderBy + ' ' + @orderByDirection + '), 
       [Id],[Provider],[RequestIdentifier],[RequestData],
       [ResponseData],[UserName],[AccountName],[AccountId],
       [CreatedUserId],[CreatedDate],[MethodName]
    FROM dbo.LogAPICall'
    + CASE WHEN LEN(@searchString) > 0 THEN
    ' WHERE Provider LIKE ''%'' + @searchString + ''%''
       OR MethodName LIKE ''%'' + @searchString + ''%'''
       ELSE '' END
    + ') AS x 
       WHERE rn BETWEEN @displayStart 
         AND @displayStart + @displayLength - 1;';

  DECLARE @params NVARCHAR(MAX) = N'@searchString NVARCHAR(1000),'
    + '@displayStart INT, @displayLength INT';

  EXEC sp_executesql @sql, @params, @searchString, @displayStart, @displayLength;
END
GO

Especially if you have the setting optimize for ad hoc workloads enabled. Now, this is still prone to SQL injection due to the dynamic ORDER BY, which can't be parameterized, so you may want to add validation that these parameters only contain valid values.