6

I am looking for a way to override the SaveChanges method/process in EF. We need to grab the SQL somehow, prevent the normal update/delete/insert from executing, and use that generated SQL to run our custom procedure.

  • Call SaveChanges() as normal. Let EF generate the SQL.
  • Get the SQL
  • Prevent that SQL from being executed the normal way
  • Call a custom stored procedure (takes additional params, etc)
  • Pretend like we executed SaveChanges (or just return 0)

The only real problem I see is grabbing the SQL from inside the SaveChanges method. What we would do is something like this, ideally...

  1. Get provider / connection / etc
  2. Setup event hooks to handle this
  3. Done, no code changes / overrides, etc.

We are using MVC4 & EF5 against an 3 letter acronym's database. The point here is to avoid hand-coding SQL in each update action, and rely on EF to generate all that for us. Since the procedure takes straight SQL

Yes, this is not a good way to do it (the single procedure) but we have no choice in the matter. None whatsoever. If we can't do this then we will need to write the custom sql. Perhaps there is another way that we can enforce this, where we pass the context and do the work ourselves? Then we can just audit that 'SaveChanges()' is never called :D

Solution


I used the EFTracingProvider as a starting point to create my own provider that does this (and some other things). You can also do it with only the EFTracingProvider by placing everything in your Entities class and handling events. You won't see your modified SQL since this event will fire after it, so you need to do your own logging. This has been stripped down to better fit in the website :)

public class MyEntities : MyBaseEntities
{

    public MyEntities(): this(connectionString: "name=MyBaseEntities") {}
    public MyEntities(string connectionString) 
             : base(MakeConnection(connectionString, "EFTracingProvider")) {}

    /// <summary>
    /// Insert the wrapped connection by calling the base toolkit.
    private static EntityConnection MakeConnection(string connectionString, params string[] providers)
    {
        var conn = EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
            connectionString,
            providers
            );

        //get the tracing connection, so that we can attach event handlers
        var us = conn.UnwrapConnection<EFTracingConnection>();
        if (us != null)
        {
            us.CommandExecuting += BeforeExecute;
        }
        return conn;
    }

    private static void BeforeExecute(object sender, CommandExecutionEventArgs e)
    {
        // If an Create/Update/Delete action then we need to wrap it in our custom proc   
        if (IsCudAction(e.CommandTree))
        {
            var text = cmd.Parameters.Cast<DbParameter>().Aggregate(
                cmd.CommandText, 
                (current, p) => current.Replace(p.ParameterName, SafeSql.Prepare(p.Value)));

            cmd.CommandType = CommandType.StoredProcedure;
            cmd.CommandText = "[dbo].[ExecuteForMe]";
            cmd.Parameters.Clear();
            cmd.Parameters.AddRange(new[]
                {
                    new SqlParameter("commandText", text),
                    new SqlParameter("extraInfo", "logging context")
                });
        }
    }

    public static bool IsCudAction(DbCommandTree commandTree)
    {
        if (commandTree is DbUpdateCommandTree) return true;
        if (commandTree is DbDeleteCommandTree) return true;
        if (commandTree is DbInsertCommandTree) return true;
        if (commandTree is DbQueryCommandTree) return false;
        if (commandTree is DbFunctionCommandTree) return false;
        throw new InvalidOperationException("Unknown type of CommandTree: " + commandTree.GetType().Name);
    }
}
Andrew
  • 8,322
  • 2
  • 47
  • 70
  • 4
    Are you aware of the fact that you can [map crud actions to stored procedures](http://weblogs.asp.net/zeeshanhirani/archive/2010/10/14/inserting-updating-and-deleting-entity-using-stored-procedures.aspx)? You can use these to call your central sproc. – Gert Arnold Nov 26 '12 at 08:53
  • @GertArnold That seems to be something worth posting as an answer. I'd `+1` it, anyway... – Tieson T. Nov 26 '12 at 08:55
  • I've heard of that (not used it.) The standard (and this is a large and slightly interesting corp) is to use this ONE stored procedure that the put in all their many databases. We aren't even allowed to write procedures (and I wouldn't want to write 3 procs for ~70 tables and then manually map them too, plus maintain them). Now if EF could _generate_ those! You should add it as an answer, even if it might not turn out to be applicable for me (corporate restraints.) – Andrew Nov 26 '12 at 09:05
  • 1
    Consider what happens when an operation fails, the dbcontext changetracking will be screwed unless somehow you bubble up failures via the procedure, especially when performing multiple operations which really need transaction management – Paul Zahra Nov 26 '12 at 09:13
  • Well aware, and very worried. I'm a bit worried about all of this, honestly. Mostly that won't be an issue for us, we will discard everything on failure, so if it is all in one transaction we will be safe. Then the next request will come in, and we will pull down all new objects (even for imports, etc). – Andrew Nov 26 '12 at 09:25
  • If you have just one stored procedure to "rule them all" what parameters does this procedure take to be able to do anything? Sql? If so it smells like you may have a serious security issue (sql injection). – Pawel Nov 27 '12 at 07:27
  • @Pawel: They still haven't given us the definition. Anyway, it was explained as taking the sql to run and then some additional info. They do it for """security""". I guess it does give one place to run all modification actions through, and if you do what i'm trying to do then it's no worse. Of course, SP for every action would be the best. – Andrew Nov 27 '12 at 08:55
  • For anyone who is interested: Its a crude auditing process, basically. It turns out that we insert into "temp" tables which have extra columns for auditing and logging / error reporting, then call a separate procedure to run things. Two step process. Still have some creative code to map objects to the new "temp" tables which are not mapped as part of EF, but we re-use the metadata for that, plus everything is consistent as far as naming, patterns, proc params, and so on. – Andrew Jul 11 '13 at 02:52

2 Answers2

2

Seems that it is perhaps possible to get the SQL using EF Tracing Provider... see the last post in this link

Community
  • 1
  • 1
Paul Zahra
  • 9,522
  • 8
  • 54
  • 76
  • Cool. I have actually installed that perviously (and an auditing plugin), but haven't used it yet. I haven't played with EF in depth much, usually other frameworks. If nothing, it is a damn good place to look for hooks, and maybe a starting point for more custom stuff :) – Andrew Nov 26 '12 at 09:26
  • This gave me a good starting point, so I'll give it the answer. As the solution shows, now there is a point to grab & modify the command. I can't/didn't change the `CommandTree` type, but it seems to work regardless on SQL Server. All my CUD actions now go through the other procedure (and `sp_executeSql`, eww). – Andrew Nov 29 '12 at 10:04
  • Glad it helped. At first it seems a bit of a pants solution to have to go through one SP, to be honest I envy that with the system I'm working on at the moment that uses SPs, Scalar Functions, views, triggers, strongly typed datasets, inline sql and now EF4, i.e. it's a mess! – Paul Zahra Nov 29 '12 at 15:11
  • I wish I could use all that! This is DB2 so... ick. Talk about an anti-productivity db to use. – Andrew Nov 30 '12 at 01:55
  • Count your blessings, we've just cleared out Pervasive - now that is a pile of pants – Paul Zahra Nov 30 '12 at 10:52
2

As I said in the comment, you can map crud actions to stored procedures (well, CUD actions). Yes, it takes a lot of work to write, map and maintain these sprocs, but possible gains are better performance and security. That's why DBA's like them so much. It might even be the main reason why this central monolith procedure in your database was created. There may be a way to use "CUD sprocs" to meet the same requirements.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • Yes, it is work :) I avoided it last time by using a tool that did everything including writing the stored procedures. Each area had their own model, with only the fields they specifically used included, etc. That wasn't too bad, even with a huge model. Of course, common stuff was common. – Andrew Nov 27 '12 at 08:54