brockallen / BrockAllen.MembershipReboot

MembershipReboot is a user identity management and authentication library.
Other
742 stars 238 forks source link

On CreateAccount, EF throws an InvalidOperationException #585

Open kabua opened 9 years ago

kabua commented 9 years ago

The Bug

I will explain the bug and then a simple work-a-round.

The old Issue is a valid bug. But the bug isn't coming from the fact that @corpsekicker was creating a custom User Account. The bug is coming from the fact that DbContextUserAccountRepository's

        public override TAccount Create()
        {
            return items.Create();
        }

is called.

This call is triggered when CreateAccount is called where the account parameter is set to null. Thus, when the following code is called

            account = account ?? CreateUserAccount();

the CreateUserAccount() is executed. Which eventually calls into DbContextUserAccountRepository's

        public override TAccount Create()
        {
            return items.Create();
        }

Note that items is defined as a DbSet<TAccount>, therefore, this call creates a User Account proxy which is already associated with EF.

Then when

            userRepository.Add(account);

is called (a few lines after the account = account ?? CreateUserAccount(); line), the DbModelBuilderExtensions.RegisterUserAccountChildTablesForDelete(...) is triggered. Which runs this set of code

foreach (TAccount account in e.NewItems)
{
    account.ClaimCollection.RegisterDeleteOnRemove(ctx);
    account.LinkedAccountClaimCollection.RegisterDeleteOnRemove(ctx);
    account.LinkedAccountCollection.RegisterDeleteOnRemove(ctx);
    account.PasswordResetSecretCollection.RegisterDeleteOnRemove(ctx);
    account.TwoFactorAuthTokenCollection.RegisterDeleteOnRemove(ctx);
    account.UserCertificateCollection.RegisterDeleteOnRemove(ctx);
}

Because the account is 'known' by EF (and is marked at 'added') then none of the account's collections can be de-referenced. If they are de-referenced, as in account.ClaimCollection, then EF throws the following exception

The source query for this EntityCollection or EntityReference cannot be returned when the related object is in either an added state or a detached state and was not originally retrieved using the NoTracking merge option.

 at System.Data.Entity.Core.Objects.DataClasses.RelatedEnd.CreateSourceQuery[TEntity](MergeOption mergeOption, Boolean& hasResults)
 at System.Data.Entity.Core.Objects.DataClasses.RelatedEnd.ValidateLoad[TEntity](MergeOption mergeOption, String relatedEndName, Boolean& hasResults)
 at System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1.Load(List`1 collection, MergeOption mergeOption)
 at System.Data.Entity.Core.Objects.DataClasses.RelatedEnd.DeferredLoad()
 at System.Data.Entity.Core.Objects.Internal.LazyLoadBehavior.LoadProperty[TItem](TItem propertyValue, String relationshipName, String targetRoleName, Boolean mustBeNull, Object wrapperObject)
 at System.Data.Entity.Core.Objects.Internal.LazyLoadBehavior.<>c__DisplayClass7`2.b__1(TProxy proxy, TItem item)
 at System.Data.Entity.DynamicProxies.UserAccount_4D648538DA59C57D93498042CF9A88749A46F5408DCB6BFA60F620357F905DE6.get_ClaimCollection()
 at System.Data.Entity.DbModelBuilderExtensions.<>c__DisplayClass1`8.b__0(Object sender, NotifyCollectionChangedEventArgs e) in c:\\SW\\3rdParty\\ThinkTecture\\MembershipReboot\\BrockAllen.MembershipReboot-master\\src\\BrockAllen.MembershipReboot.Ef\\DbModelBuilderExtensions.cs:line 33
...

The good news - this bug is easy to work-a-round.

The Work-a-Round

Call CreateAccount with an existing account. For example

_userAccountService.CreateAccount(username, password, email, account: new UserAccount());

So, when the account.ClaimCollection is de-reference the value is null and the extension method works without complaining. Thus everything works just fine.

Happy Coding!

brockallen commented 9 years ago

Hmmm... so should the default EF repository logic change to just call new?

kabua commented 8 years ago

I just go the same issue with trying to create a Group, and I'm currently block on this issue.

I think you might be right, that perhaps the EF logic should be changed to calling 'new'.Otherwise, I don't think any of the RegisterXXXXXForDelete will work as expected.

Or perhaps just change:

        public override TGroup Create()
        {
            return items.Create();
        }

to

        public override TGroup Create()
        {
            return items.AsNoTracking().Create();
        }

So that the new item isn't being tracked, I'm testing it now. I'll let you know what I find.

kabua commented 8 years ago

That didn't work because I was working on a cached file and didn't realized that I couldn't call Create() after .AsNoTracking(). So, I tried this:

            var item = items.Create();
            db.Entry(item).State = EntityState.Detached;

            return item;

but that didn't work. However, this did:

        public override TGroup Create()
        {
            return new TGroup();
        }

Had to change

where TGroup : RelationalGroup

to

where TGroup : RelationalGroup, new()

Hope that helps.