-
Notifications
You must be signed in to change notification settings - Fork 300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SqlConnection.CreateCommand() - Auto-Enlist in Pending Local Transaction #28
Comments
As recently announced in the .NET Blog, focus on new SqlClient features an improvements is moving to the new Microsoft.Data.SqlClient package. For this reason, we are moving this issue to the new repo at https://github.com/dotnet/SqlClient. We will still use https://github.com/dotnet/corefx to track issues on other providers like System.Data.Odbc and System.Data.OleDB, and general ADO.NET and .NET data access issues. |
After a cursory check, it seems like the core essence of implementing the alternate proposal would be to change a couple lines in Change:
To something like:
|
The change is quite simple as you point out but it changes default behaviour which means it isn't backwards compatible. Given the wide usage of this library you can be certain that someone somewhere has (incorrectly) used the behaviour that would change as a certainty and would be broken by changing it. That said, the move from System.Data.SqlClient to Microsoft.Data.SqlClient isn't drop-in compatible so it does provide an idea opportunity for such a change. |
This would be very useful just having |
cc @saurabh500 |
This such basic and useful thing (and most likely very easy to implement for anyone slightly involved in developing the SqlClient) is pending since 2016. And now was again moved to a new milestone. Quite amazing! |
I see this sort of comment far too often. If it's so easy why don't you contribute a fix for it instead of complaining? |
@CodeAngry you should be grateful that it is planned for the next milestone, it could have been Future or "won't fix' |
@ericstj The problem is not whether it is or is not planned or if I should be grateful. I coded against the stream on this for years now always having to drag the Transaction around when create commands. The problem is the milestone can keep on moving. It just moved again. It can be 3 more years before anything happens on such small but very useful feature. It's always the Microsoft way. It's all perfect but there's always some quality of life thing missing that baffles anyone that has used MS APIs or Software long enough and also makes you write a lot of pointless extra code. @Wraith2 If you see this sort of comment far too often... it should make you think Nobody likes to waste time writing comments on Github. I'll take a look at the code during this weekend. But as I said, if I was on that team, already knowing the code, this should be a very quick and logical fix (I would not call it improvement). I'll also check back in a few years to see how this goes. |
I need to ask a stupid question. Looked at the
or
Some ways to change this:
Keep in mind this feature hardly affects regular SqlClient users. Library (reusable code) writers would benefit the most from this. |
Above, there's a mention of contributing a fix for this...would Microsoft be interested in a pull request that addresses this? In other words, is the general idea conceptually agreeable--just there's a need for a resource to implement it? |
@bgribaudo MS absolutely accepts pull requests, I have submitted and gotten accepted several to this repo |
You have to consider backwards compatibility in proposed changes. If you make new behaviors the default, it is very likely that you will affect the behavior of existing applications. My suggestion is to think about ways to implement this while maintaining the existing behavior by default and have the new behavior opt-in somehow. |
If the code was changed so when the From what I can tell, Transaction set by the user is checked anyway to belong to the Connection and also the Connection is checked to see if the Transaction matches. So... why is it required to be specified by the user, I don't know? It's like the library saying: Tell me the transaction, so I can test you. I already know it and I'm still gonna use my own value, but I'll test you and throw if you fail. Funny thing is the library nullifies the internal transaction when it completes so it takes this responsibility away from the user. This strange redundant design choice leads me to think it's either historical or it's used somewhere else. Because, since this value is apparently useless, what I'm worried is that there's some voodoo functionality that relies on this I'm gonna spend the new few days changing the code and testing these solutions and seeing if it works. All I need to prove is this value is not used outside of double-checking the user. If it works, even if I can't push it here, at least I'll use it for myself as I write a lot of SqlClient wrapper Apis and I need to stop worrying about the Transaction. |
So I fixed this in 2 lines of code. And wrote a simple test with a transaction'less command and it works. Added this property to
And changed this line in
to
And my initial test worked. In my My test code:
Is this too easy?! I'll run some more checks. |
Hi @CodeAngry A small bit of info for first time contributors:
|
Thanks a lot for this. I kept testing and got a more easy solution. I'll follow these steps during the weekend with a proper branch and the new solution. @cheenamalhotra But, how does one pass all tests? I pass most tests except some strange failures like: Test
while my actual exception text is:
In my exception, these 2 are reversed: 'AEAD_AES_256_CBC_HMAC_SHA256', 'AES_256_CBC' from the expected. But they are properly ordered alphabetically while the expected seems to be ordered by length. |
This test case I would recommend setting up environment first with 'dotnet/master' branch and then running tests over your branch. This way you'd know what tests are flaky and you shouldn't worry about. Also remember to run tests with Admin Command Prompt to ensure AE tests can install certificates and not fail for you. |
@roji Have you seen similar ask in NpgSql driver? @ajcvickers do you have any context on whether this kind of convenience API was ever discussed earlier? |
@saurabh500 and others, Npgsql behaves much like the proposed behavior. PostgreSQL only supports a single transaction on a given connection at a given time, and if a transaction has been started then commands must be executed within it. So with Npgsql you don't need to manually assign the Transaction property of DbCommand - commands are always implicitly executed within the transaction. One potential drawback of doing this regards compatibility with other ADO drivers which do support multiple transactions on the same connection. In those providers it absolutely makes sense to require transactions to be explicitly set (and not setting them may mean to execute outside of any currently-active transaction). Therefore, to promote portability between providers, it may be better to leave the requirement even for providers where it isn't necessary (even though again, Npgsql already works this way). However, this doesn't seem like a very powerful argument (providers already contain various points of incompatibility which are more serious anyway). If the primary issue here is passing the DbTransaction down the stack (as @bgribaudo wrote above), then an alternative solution is to use System.Transactions which removes this exact pain point - connections/commands enlist implicitly in the ambient TransactionScope. This is one reason why I don't really see much value in making this change in SqlClient. If it is decided to make this change, then I'm not sure I see the point of making it opt-in with a new public surface flag (CommandAutoTransaction). Unless I've missed something this wouldn't be a breaking change as previously-throwing invocations (transaction in progress but not set on the command) would start working. So if this is desirable, I'd just make the change and be done with it. PS FWIW I'd avoid using the term "enlistment" here, since that's usually used in the context of System.Transactions (i.e. TransactionScope), where here we're just talking about plain old DbTransaction. |
@CodeAngry start a PR. That is a better way to look at code and comment on it if you want comments on ongoing work. Mark it as WIP in case you need time to complete the work. I haven't looked at the code but few items to considered:
This seems like a suggestion where if a connection is marked to auto enlist its commands into a transaction then whether connection.CreateCommand() is used to create the command is enlisted into the connection then assignment of connection with the set property should lead to enlisting of command in the transaction as well. |
@roji, so the |
I'm thinking about starting a new issue with this and have the first message clearly explain everything. @cheenamalhotra should I do that or just drop it all as a post in here when the change is ready? I made another variant last night that does not need the opt-in flag and can be/is fully stealthy. So a null SqlCommand.Transaction works just as well. I still need to do a search for any reflection use anywhere just for my peace of mind. And pass the passable tests. @saurabh500 @roji This change would not affect any other implementation for another DB. It's a SqlServer Client only quality of life change. |
@saurabh500 We have definitely talked about this experience being something that we want to improve, and we have discussed how the Npgsql approach seems to work well, at least for Postgres. Unfortunately, I don't remember if the discussion went beyond that. I'm certainly in favor of making this better. Also, after reading @roji's post I'm not trying to remember all the reasons we recommend people not to use System.Transacations. This is where I really miss Diego! |
Finalized the changes. The branch is: https://github.com/CodeAngry/SqlClient/tree/fix-issue-28 Given that I'm new to contributing, I'll need to be advised on how to proceed. PurposeRemove any awareness or dependency for PreambleBefore I started to code this I tested to confirm parallel transactions are not supported. And you can't begin transaction client-side twice on a connection before you end the previous transaction. This means each So each connection has a transaction, each command has a connection, so why does a command need to know its transaction? Changes Description
Moved the validation into assignments, constructors and execution calls. But if the user never touches the Tests pass but I could not test Azure side of tests. I really hope these changes get checked by the developer that knows this library. I'm talking about the dev that understands how all moving parts in this library work together and can easily figure out if these changes touch anything I did not notice or if my assumptions are correct. The I'll detail the changes added here: Library Changes
Tests ChangesFixed some failing tests not affected by the changes.
And also modified all tests (about 3) that checked to ensure the behavior that was changed in this branch. So all tests testing assigning other connection's transaction and null transaction behavior no longer apply. Still need to write some tests and publish them. I have written tests but I made a new project for them and use Backward compatibilityUsers should not be affected by the changes (on sane coding practices). Reading the The only difference in backward compatibility is this (the assert tests say the story):
ConclusionI don't think more changes are needed to the code. Someone should still double-check my validation patterns and make sure they are consistent with overall practices. I also have a bunch of comments on the library.
And, finally, even if these changes do not get published, I'm using them from now on. :) My work when I write SqlClient backend apis (without EF I write mostly sproc-only) is much easier not having to drag the transaction around. |
@roji and @ajcvickers Thanks for the inputs.
Do you intend to send a Pull Request? |
Also, if you are modifying both SqlCommand and SqlBulkCopy, I recommend sending two different PRs so that the changes are easier to review. |
@saurabh500 I'll send it tomorrow. I'll also get some tests ready. But I have all the changes in one single branch. Not sure how I can PR partially. Changes are quite small in functions touched. You can see it all here and advise me on how to proceed: https://github.com/CodeAngry/SqlClient/tree/fix-issue-28 So there's changes on:
Problem is it's all quite linked so it's better to go as a whole. One other question is: should I add more comments in the code? I tried to explain things while not writing novels. I'll comment much more in the tests, anyway. |
Yeah, it is. |
@CodeAngry Would that mean that this is bringing breaking changes for existing applications or the tests would also pass without any test source modifications? |
@cheenamalhotra So, let's take this test: using var connection = new SqlConnection();
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "select getutcdate();";
using var transaction = connection.BeginTransaction();
// Command.Transaction = Transaction; // missing assignment is fail condition
Assert.Throws(()=> command.ExecuteNonQuery()); // throws as command lacks transaction ^ This test checks that a command with no transaction belonging to a connection with an open transaction fails when executed. Such test existed. This test no longer applies because it can no longer fail. I changed all of them and added a whole bunch of new tests. I'm in the process of porting them from my quick MSUnit project to the internal xunit one and from my C# 8.0 syntax to 7.3 and
^ This is nice! |
@CodeAngry Why should the test change? (unless the only test that is changing is that Command Execution succeeds without any additional transaction assignment to the command) What should change is that if the transaction is not explicitly assigned, then the command inherits from the connection. Also I see new exceptions being surfaced and some exceptions being removed from SqlBulkCopy constructor. That is another concern. I don't think there should be any additional changes apart from the fact that it is more convenient to use SqlCommand and SqlBulkCopy without any additional transaction assignments. |
Also can we make this change just about changing SqlCommand to be more convenient to use. I see new surface area being exposed like |
@saurabh500 You can see the test changes right now in the commit: Only changes are in the following tests:
Maybe I did not express myself clearly. No test is removed. Some lines from them are changed or removed. You can see the diffs. No test removed, just a few lines dropped or edited as they didn't apply anymore. |
@CodeAngry in general it is better to submit a pull request rather than pointing at a commit - github has great code review functionality on PRs. I also agree with @saurabh500 that changes should be very minimal and one step at a time, i.e. not introduce CurrentTransaction since that isn't directly related to what's being discussed here. |
@roji I created PR #330 from @CodeAngry 's active branch for issue, for better reviewing. |
@Wraith2 : Because as you can see by now PRs are usually not taken. |
The majority of my PR's have been merged and where they haven't I understand why. I would like it to take less time to get them merged but they get there. Where is the PR that covers this issue? Perhaps it just needs clarification on why it isn't being accepted. |
@Wraith2 : Because it's a breaking change. I'm just waiting for my team to say "enough" and start forking for things that are clearly broken. |
Anyone who wants a fix for this is to never use MARS, SqlTransaction, Scoped Transaction and always use If you want just create a |
@CodeAngry : Beware; this will haunt you unless you disable connection pooling: #84 |
@jhudsoncedaron I always do. MARS too. Thanks! And... look, a 2016 bug still OPEN. I'm dying here with laughter. Literally measuring time in years/half decades for library bug fixes. It must be nice working at Microsoft. At my work, I actually need to get things done within REASONABLE timeframes. |
That bug is actually two decades old. Framework has another viable workaround for it though. But the workaround doesn't seem to work in Core. |
One important note about the interaction between transactions and MARS. I'm no expert, but setting SqlCommand.Transaction is an explicit opt into allowing two commands to participate in the same transaction when MARS is enabled; it accepts that a rollback may undo both commands. Compare the situation with savepoints - where there is no possible opt-in - described in this article. I haven't fully explored this so I'm not saying anything definitive; but before removing the requirement to set SqlCommand.Transaction, it's a good idea to get a full understanding of the situation. |
When CreateCommand() is called on a SqlConnection that is in a transaction, the SqlCommand returned by that method is not associated with the connection's transaction. Instead, the command must be explicitly informed of the transaction or an exception similar to the following will be thrown when the command is executed:
Cost of the Manual Wire-Up
The need to manually associate the command with the connection's transaction adds a small amount of code clutter.
More significantly, the need to explicitly set up this association when a transaction is used means that modifying a high-level component to use transactions requires code changes to lower-level components sharing the same SqlConnection, even when these lower-level components are transaction-agnostic.
All methods executing SqlCommands on the connection when the transaction is in use must have access to the transaction object. SqlConnection does not provide a public property exposing its transaction, so the object either must be passed around or accessed via reflection.
Proposal
When CreateCommand() is called on a SqlConnection in a transaction, the SqlCommand it returns should be associated with the connection's transaction, eliminating the need for the explicit wire-up.
[Added: 2019-06-03]:
Alternately (perhaps better yet), change command execution so that it automatically uses the transaction associated with the connection when it's executed. With this approach, any command (whether or not created by CreateCommand()) will execute against the proper transaction. Existing logic requires that the connection's transaction is used; the change proposed here is for the command to automatically use this transaction vs. the current behavior of the command throwing an exception when the transaction hasn't been explicitly referenced.
The text was updated successfully, but these errors were encountered: