简体   繁体   中英

Returning IQueryable from Web API methods using SQL Server Impersonation

I have a .NET Framework 4.6.1 WebAPI that exposes endpoints to perform CRUD operations. My client wants all operations performed in the context of an individual user account so I'm using SQL Server impersonation to make it work (all statements are written to an audit file so using SQL Impersonation allows us to see who is executing the statement). I get the username from the NameIdentifier claim and then execute statements using the “EXECUTE AS USER” command.

An example to retrieve a single record via an ID is the following:

using (var transaction = SyncDbContext.Database.BeginTransaction())
{         
   await SyncDbContext.Database.ExecuteSqlCommandAsync($"EXECUTE AS USER = '{UserName}'");
   var data = await LookupAsync(id);
   await SyncDbContext.Database.ExecuteSqlCommandAsync("REVERT");
   transaction.Commit();
   return data;
}

The statements need to be performed inside a transaction otherwise SQL Server throws an exception. The audit file for this operation would contain the statements EXECUTE AS USER = [USERNAME],[Statement], REVERT

All CRUD operations work except for endpoints that return an IQueryable. I need to return an IQueryable to support the oData specification which allow the client to pass query string arguments to modify the query (eg, a query string parameter containing $top=10 will become.Take(10) in the EF query).

The following is example of an endpoint that returns an IQueryable:

    [EnableQuery(MaxTop = 1000)]
    public async Task<IQueryable<Employee>> GetEmployees()
    {
        using (var transaction = SyncDbContext.Database.BeginTransaction())
        {
            await SyncDbContext.Database.ExecuteSqlCommandAsync($"EXECUTE AS USER = '{UserName}'");
            var data = SyncDbContext.Employees.AsQueryable();
            await SyncDbContext.Database.ExecuteSqlCommandAsync("REVERT");
            transaction.Commit();
            return data;
        }
    }

The problem is that due to the deferred execution of iQueryable the audit file contains the statements out of order - EXECUTE AS USER = [USERNAME], [REVERT], [STATEMENT]. Is there a way to make the IQueryable statement “execute" with the transaction or a better way of achieving my goal?

Is there a way to make the IQueryable statement “execute" with the transaction or a better way of achieving my goal?

Yes, just use ToList() to convert the query to an actual list of results. This will of course fetch the entire Employees table to the client and do the filtering in the application, and that is usually not a good idea.

An alternative would be to take a parameter that describes how the query should be done

[EnableQuery(MaxTop = 1000)]
public async Task<List<Employee>> GetEmployees(Func<IQueryable<Employee>, Task<List<Employee>>> filter)
{
    using (var transaction = SyncDbContext.Database.BeginTransaction())
    {
        await SyncDbContext.Database.ExecuteSqlCommandAsync($"EXECUTE AS USER = '{UserName}'");
        var data = await filter(SyncDbContext.Employees.AsQueryable());
        await SyncDbContext.Database.ExecuteSqlCommandAsync("REVERT");
        transaction.Commit();
        return data;
    }
}

Now the caller gets to specify whatever kind of filtering he wants, while still ensuring the query is run within the transaction. Alternatives would be to take a Func<Employee, bool> for use in a Where-statement. Or a Func<IQueryable<Employee>, Task<T>> in case the caller wants to be more specific.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM