0

I have a query I would like to run via C# application. There is no option to do this outside of the application. I have the following code:

var keyGroupsToCleanUp = new List<string>
{
    "Address",
    "Manufacturer",
    "Product",
    "Customer",
    "Picture",
    "Category",
    "Vendor",
    "SS_A_Attachment",
    "SS_A_AttachmentDownload",
    "SS_MAP_EntityMapping",
    "SS_MAP_EntityWidgetMapping",
};

foreach (var keyGroup in keyGroupsToCleanUp)
{
    _databaseFacade.ExecuteSqlCommand($@"
    DELETE
    FROM GenericAttribute
    WHERE KeyGroup = {keyGroup} AND [Key] = 'CommonId'
    AND EntityId NOT IN (SELECT Id FROM [{keyGroup}]);
    ");
}

I want to loop through each name in the List and run the below query for each of them. When I try to do this, I receive the following error:

System.Data.SqlClient.SqlException (0x80131904): Invalid object name '@p1'.

From what I have gathered after searching online, this is because a Table name cannot be a string. You have to declare a variable and use this variable for the table name. I learned that a Table variable has columns that need to be declared and felt a wave of dread wash over me. None of these tables have the same column structure.

Is what I am trying to do possible? If so, how can I do it?


The GenericAttributes table is one large table that consists of six columns. enter image description here

When I joined the project that this is being used on it had already been used to the point where it was irreplacable. You can save additional data for a database table in here by specifying the KeyGroup as the Database table. We have a table called "Address" and we save additional data in the GenericAttributes table for the Address (It does not make sense, I know). This causes a lot of issues because a relational database is not meant for this. The query I have written above looks for rows in the GenericAttributes Table that are now detached. For example, the EntityId 0 does not exist as an Id in Address, so it would be returned here. That row must then be deleted, because it is linked to a non-existant entityId.

This is an example of a query that would achieve that:

// Address
_databaseFacade.ExecuteSqlCommand(@"
DELETE
FROM GenericAttribute
WHERE KeyGroup = 'Address' AND [Key] = 'CommonId'
AND EntityId NOT IN (SELECT Id FROM [Address]);
");

I have to do this for 11 tables, so I wanted to make it a bit easier to do. Every query is written in the same way. The only thing that changes is the KeyGroup and the table that it looks for. These will both always have the same name.

Here is an example of another call for Products. They are the same, the only difference is the KeyGroup and the Table in the NOT IN statement.

// Product
_databaseFacade.ExecuteSqlCommand(@"
DELETE
FROM GenericAttribute
WHERE KeyGroup = 'Product' AND [Key] = 'CommonId'
AND EntityId NOT IN (SELECT Id FROM Product);
");
shinyshark
  • 81
  • 8
  • I understand your question. The SQL command works when I enter it in SSMS. this is a query that is used for a table concept from the NopCommerce framework. It is called the GenericAttributes table and it is an abomination I wished I had never seen. You can read more about it here: https://docs.nopcommerce.com/en/developer/tutorials/db-schema.html – shinyshark Nov 18 '21 at 13:23
  • @Charlieface Could you give an example of how to do this or is it just plain impossible? – shinyshark Nov 18 '21 at 14:00
  • I have updated the question explaining why I would want to do this. I hope it makes sense to you. The table in question for this issue was misused horribly by the project developers before I even came here. – shinyshark Nov 18 '21 at 14:56

3 Answers3

1

At a guess, you're using Entity Framework Core. The ExecuteSqlCommand method accepts a FormattableString, and converts any placeholders into command parameters. But your placeholders appear to be column/table names, which cannot be passed as parameters.

Since there's also an overload which accepts a string, which has different behaviour, this method has been marked as obsolete, and replaced by ExecuteSqlInterpolated and ExecuteSqlRaw.

Assuming none of your values can be influenced by the user, and you're happy that you're not going to introduce a SQL Injection vulnerability, you can use ExecuteSqlRaw instead:

_databaseFacade.ExecuteSqlRaw($@"
    DELETE
    FROM GenericAttribute
    WHERE KeyGroup = [{keyGroup}] AND [Key] = 'CommonId'
    AND EntityId NOT IN (SELECT Id FROM [{keyGroup}]);
    ");
Richard Deeming
  • 29,830
  • 10
  • 79
  • 151
  • Hello Richard, I assume I need a Nuget Package for this called 'Microsoft.EntityFrameworkCore.Relational'? I tried installing it, based on another answer. My project is not compatible with this. I have a netcoreapp2.1 application. I will look into porting my application over to a newer version. – shinyshark Nov 18 '21 at 12:08
  • If you're using Entity Framework Core, you should already have that package installed. If you're still using Entity Framework 6, then my answer doesn't apply. Which ORM are you using? – Richard Deeming Nov 18 '21 at 12:10
  • From the docs: *"never pass a concatenated or interpolated string (`$""`) with non-validated user-provided values into `FromSqlRaw` or `ExecuteSqlRaw`"* Just use `ExecuteSqlInterpolated` properly – Charlieface Nov 18 '21 at 13:18
  • @Charlieface Hence my warning about SQL Injection. You can't pass a table or column name as a parameter though. – Richard Deeming Nov 18 '21 at 14:34
1

To ensure there is no injection vulnerability, you can use dynamic SQL with QUOTENAME

_databaseFacade.ExecuteSqlRaw(@"
DECLARE @sql nvarchar(max) = N'
    DELETE
    FROM GenericAttribute
    WHERE KeyGroup = @keyGroup AND [Key] = ''CommonId''
    AND EntityId NOT IN (SELECT Id FROM ' + {0} + ');
';

EXEC sp_executesql @sql,
    N'@keyGroup nvarchar(100)',
    @keyGroup = {0};
    ", keyGroup);

Note how ExecuteSqlRaw will interpolate the string. Do not interpolate it yourself with $

Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • Thank you for your response. I appreciate your patience with a noobie like me. I copied your code and put it in the foreach look. It states that ExecuteSqlInterpolated only uses one parameter but is invoked with two. I removed the ", keyGroup" at the end. After trying that, it says "argument type 'string' is not assignable to parameter type 'System.FormattableString'". I looked up what a FormattableString is, but I don't see what the error is trying to say. Is the query a FormattableString? – shinyshark Nov 18 '21 at 15:16
  • I tried the query with ExecuteSqlRaw and noticed that the 'CommonId' was wrong syntax. Without changing anything it gave the error: "Microsoft.Data.SqlClient.SqlException (0x80131904): Incorrect syntax near 'CommonId'. Must declare the scalar variable "@sql"." I then removed the single qoutes around CommonId and ran it again and got the following error: "Microsoft.Data.SqlClient.SqlException (0x80131904): Must declare the scalar variable "@keyGroup"." – shinyshark Nov 18 '21 at 15:47
  • I had a look around, it looks fine to me but it said "Microsoft.Data.SqlClient.SqlException (0x80131904): Incorrect syntax near ')'.". I played around a bit with it, but I couldn't find the error. – shinyshark Nov 18 '21 at 16:00
  • 1
    Extra comma, fixed now – Charlieface Nov 18 '21 at 16:10
  • Thank you so much! It worked, and now I can study it and see what I should do in the future! – shinyshark Nov 18 '21 at 16:23
-1

Try following:

foreach (var keyGroup in keyGroupsToCleanUp)
{
    var sql = @"DELETE FROM GenericAttribute
                WHERE KeyGroup = @Group
                AND [Key] = 'CommonId'
                AND EntityId NOT IN (SELECT Id FROM @Group)"; // Or [@Group], depends on schema

    _databaseFacade.ExecuteSqlCommand(
        sql,
        new SqlParameter("@Group", keyGroup));

This code assumes, that ExecuteSqlCommand in your facade follows standard Microsoft pattern (same overrides as Microsoft's ones).

quain
  • 861
  • 5
  • 18
  • I got an error: "System.InvalidCastException: The SqlParameterCollection only accepts non-null SqlParameter type objects, not SqlParameter objects." – shinyshark Nov 18 '21 at 11:26
  • 1
    This could be because you might used older Sql Client. Change `using System.Data.SqlClient` to `using Microsoft.Data.SqlClient` in your project where applicable. This needs `` in your `.csproj` file (version might be different, use the latest nuget). – quain Nov 18 '21 at 11:30
  • I looked up the error before posting back my response and found the same post you are referencing. I was using Microsoft.Data.SqlClient to begin with. There is no reference to System.Data.SqlClient in my entire project. – shinyshark Nov 18 '21 at 11:35
  • 1
    Only other thing that comes to mind is [this](https://stackoverflow.com/a/58212707/12100772). – quain Nov 18 '21 at 11:39