Print Page | Close Window

Key is already present in this dictionary error

Printed From: IdeaBlade
Category: Cocktail
Forum Name: Community Forum
Forum Discription: A professional application framework using Caliburn.Micro and DevForce
URL: http://www.ideablade.com/forum/forum_posts.asp?TID=3682
Printed Date: 20-Sep-2025 at 10:52am


Topic: Key is already present in this dictionary error
Posted By: jlozina
Subject: Key is already present in this dictionary error
Date Posted: 01-Oct-2012 at 7:01am
Hi
 
I am using an int as the primary key and it is genrated by the database. When I create one record it works fine but when I try to create a second record with a unit of work I get the following error. This happens when I clear the unit of work or set it to null.
 
System.ArgumentException was unhandled by user code
  HResult=-2147024809
  Message=Key is already present in this dictionary
  Source=Cocktail
  StackTrace:
       at Cocktail.WeakRefDictionary`2.Add(TKey key, TValue value)
       at Cocktail.ObjectManager`2.Add(TKey key, T obj)
       at Services.Order.WizardUnitOfWorkManager.Services.IWizardUnitOfWorkManager<Services.IWizardUnitOfWork>.Add(Int32 , IWizardUnitOfWork )
In my enity class the Id is set up this way
[DataMember]

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]

[Required]

public int Id { get; set; }

I noticed that the sample app uses a Guid for the primary key and DatabaseGeenratedOption is none. Is it possible to use the UnitOfWork with an int as the primary key?
 
Regards,
Joe
 



Replies:
Posted By: mgood
Date Posted: 01-Oct-2012 at 1:03pm
You need a unique surrogate or business key if you want to manage multiple units of work as demonstrated in TempHire and still use DB generated keys.

The issue with DB generated keys is that entities get a temporary key until they are saved. This violates the fundamental principle that says the identity of an entity should never change. The temporary Ids are only unique withing a single EntityManager/Unit of Work. They typically start at -100 then -101 and so on. If you create a second EntityManager/UoW it also starts at -100. That's the issue you are having here. 

If you use DB generated keys, you'll be constantly figuring out how to reconcile things in the UI. You'll need something else in your entities that is unique and can be used as the key to identify the corresponding unit of work. You can't use the temporary Ids, because they are not guaranteed to be unique. 

We generally advise against DB generated keys. They do not agree with the rich client computing paradigm where entities are created on the client. The Ids should be generated where the entity is created and once generated it shouldn't change, so the entity can always be uniquely identified. There are two ways to do this. Use a central key service or use Guids.


Posted By: jlozina
Date Posted: 02-Oct-2012 at 3:07am
If i used a Sequence(Sql Server 2012) as a central key service like

CREATE SEQUENCE dbo.OrderSequence

START WITH 1000

INCREMENT BY 1 ;

SELECT NEXT VALUE FOR dbo.OrderSequence;

how would I call it using code first?
 
Are there any other ways to use a central key service if I want to continue using an int as a surogate key?


Posted By: smi-mark
Date Posted: 02-Oct-2012 at 8:57am
I'm looking at replacing our db generated keys with such a key service.

I was planning on making an IKeyService interface, with a fakestore implementation that would just increment, and a real implementation that would call a remote service method to get the next, or next range of keys.

If you used the table based id generator from previous versions of devforce, it would do something similar. However it would only update the key on save, as far as I remember.


Posted By: smi-mark
Date Posted: 02-Oct-2012 at 5:34pm
 I just wrote a little service today to do this, for my project. So far I've only done a fake store implementation. For a real implementation you would simply use a remote service method, and then on the server side execute your query to get the next id.


    public interface IKeyService<T> : IHideObjectMembers
    {
        Task AllocateRange(int count);
        Task<T> GetNext();
    }



    public class FakeStoreIntegerKeyService : IKeyService<int>
    {
        private int _nextId;
        private readonly Queue<int> _availableKeys;

        public FakeStoreIntegerKeyService()
        {
            _nextId = 1;
            _availableKeys = new Queue<int>();
        }

        #region Implementation of IKeyService<int>

        public Task AllocateRange(int count)
        {
            for (var i = 0; i < count; i++)
            {
                _availableKeys.Enqueue(_nextId++);
            }
            return TaskFns.FromResult(true);
        }

        public Task<int> GetNext()
        {
           if (_availableKeys.Count == 0)
               AllocateRange(1);

            return TaskFns.FromResult(_availableKeys.Dequeue());
        }

        #endregion
    }


I have multiple models so I just made an interface for this specific model so I'm not sharing keys between different models. You could bypass this step.


    public interface IDemoKeyService : IKeyService<int>
    {

    }

  public class DemoFakeKeyService : FakeStoreIntegerKeyService, IDemoKeyService
    {

    }


I then subclassed Factory<T> to include my Key Service


public class DemoFactory<T> : Factory<T>
        where T : Entity
    {
        private readonly IDemoKeyService _keyService;

        public DemoFactory(IEntityManagerProvider<DemoEntities> entityManagerProvider, IDemoKeyService keyService)
            : base(entityManagerProvider)
        {
            _keyService = keyService;
        }

        public override async System.Threading.Tasks.Task<T> CreateAsync(System.Threading.CancellationToken cancellationToken)
        {
            var entity = await base.CreateAsync(cancellationToken);
            cancellationToken.ThrowIfCancellationRequested();
            var key = await _keyService.GetNext();
            cancellationToken.ThrowIfCancellationRequested();
            SetKey(entity, key);
            return await TaskFns.FromResult(entity);
        }

        private void SetKey(T entity, int key)
        {
            var keyProperty = entity.EntityAspect.EntityMetadata.KeyProperties.FirstOrDefault();
            entity.EntityAspect.SetValue(keyProperty, key);
        }
    }


In my sample project I have a Customer and a CustomerAddress table. Previously I could do Customer.AddAddress() but I wasn't sure the cleanest way to implement that anymore. So I simply made a new Factory class to handle this:


    public class CustomerFactory : ICustomerFactory
    {
        private readonly DemoFactory<Customer> _customerFactory;
        private readonly DemoFactory<CustomerAddress> _customerAddressFactory;

        public CustomerFactory(IEntityManagerProvider<DemoEntities> entityManagerProvider, IDemoKeyService keyService)
        {
            _customerFactory = new DemoFactory<Customer>(entityManagerProvider, keyService);
            _customerAddressFactory = new DemoFactory<CustomerAddress>(entityManagerProvider, keyService);
        }

        #region Implementation of ICustomerFactory

        public Task<Customer> CreateCustomer()
        {
            return _customerFactory.CreateAsync(CancellationToken.None);
        }

        public async Task<CustomerAddress> CreateAddress(int customerId)
        {
            var address = await _customerAddressFactory.CreateAsync();
            address.CustomerId = customerId;
            return await TaskFns.FromResult(address);
        }

        #endregion
    }


I then expose the ICustomerFactory on my UnitOfWork. I can then simply do:


 Address = await UnitOfWork.CustomerFactory.CreateAddress(Customer);


If Marcel has a better way of doing this, I'm all ears. Hopefully this will give you enough to get started though.





Posted By: mgood
Date Posted: 03-Oct-2012 at 5:46am
Mark,
That looks pretty good. The one thing I would change is to not pass the entire Customer entity to CreateAddress. That method doesn't need a whole customer, it only needs the id.
 
public async Task<CustomerAddress> CreateAddress(int customerId)
{
   var address = await _customerAddressFactory.CreateAsync();
   address.CustomerId = customerId;
   return await TaskFns.FromResult(address);
}


Posted By: smi-mark
Date Posted: 03-Oct-2012 at 6:11am
Good point. I have changed that now.

Can you change the Factory class to also find internal methods? I've set InternalsVisibleTo to my DomainServices project, but of course Factory<> only looks for Public | Static right now. I will add it as a task on the tracker.



Print Page | Close Window