9

I am using the UnitOfWork pattern to abstract database access in my Asp.Net application. Basically I follow the UnitOfWork pattern approach described here:

https://chsakell.com/2015/02/15/asp-net-mvc-solution-architecture-best-practices/

However, I'm struggling to understand, how I will get the Id of a newly added item. Like if I want to add a new customer to my Customer repository, how will I get the customer id? The problem is that Add and Commit are decoupled, and the Id is not known until after Commit.

Is there a way to get the id of an added item, using the UnitOfWork pattern?

brinch
  • 2,544
  • 7
  • 33
  • 55
  • 2
    The great question here is: why do you need this ID? I didn't read the link, but this smells like an architecture that doesn't help committing complete object graphs in one transaction. – Gert Arnold Jan 25 '17 at 23:22
  • I need to store the id in a cookie. – brinch Jan 26 '17 at 07:03

8 Answers8

4

Note that I dont want my EF model classes to propagate to my domain layer

I have done a workaround. I think it works pretty well

When you want a repository, for example of DbCars, and you insert a new DomainCar you want to get that Id that was only generated when SaveChanges() is applied.

public DomainCar //domain class used in my business layers
{
    public int Id{get;set;}
    public string Name{get;set;}
}

public DbCar //Car class to be used in the persistence layer
{
    public int Id{get;set;}
    public string Name{get;set;}
    public DateTime CreatedDate{get;set;}
    public string CreatedBy{get;set;}
}

First you create a generic IEntity interface and a class implementing it:

public interface IEntity<T>
{
    T Id { get; }
}

public class Entity<T> : IEntity<T>
{
    dynamic Item { get; }
    string PropertyName { get; }
    public Entity(dynamic element,string propertyName)
    {
        Item = element;
        PropertyName = propertyName;
    }
    public T Id
    {
        get
        {
            return (T)Item.GetType().GetProperty(PropertyName).GetValue(Item, null);
        }
    }
}

Then in your add method of the repository you return a IEntity of the type of your Id:

public IEntity<int> AddCar(DomainCar car)
{
    var carDb=Mapper.Map<DbCar>(car);//automapper from DomainCar to Car (EF model class)
    var insertedItem=context.CARS.Add(carDb);
    return new Entity<int>(insertedItem,nameof(carDb.Id));
}

Then , somewhere you are calling the add method and the consequent Save() in the UnitofWork:

using (var unit = UnitOfWorkFactory.Create())
{
   IEntity<int> item =unit.CarsRepository.AddCar(new DomainCar ("Ferrari"));
   unit.Save(); //this will call internally to context.SaveChanges()
   int newId= item.Id; //you can extract the recently generated Id
}
X.Otano
  • 2,079
  • 1
  • 22
  • 40
  • 2
    You're solution seems to me like a (overly?) complicated way of keeping a reference to the added object and then accessing it's `Id` when appropriate. Why not simply `var car = repo.AddCar(); unit.Save(); int newId = car.Id;`? – Dejan Oct 11 '18 at 14:46
  • 1
    caue i dont want that the code outside of the repository implementation knows about Database classes(entity framework model) @Dejan readagain my edited answer – X.Otano Oct 11 '18 at 15:11
  • For other users reading, please don't dismiss this answer as the approach respects SOLID principles in an n-tier architecture. Thanks for this, I have tried to solve this problem in various ways myself. I'll consider the above next time I face this issue – Christopher Thomas Oct 17 '18 at 10:42
4

My approach is as follows. Simply continue working with the added entity as an object. So instead of returning it's ID, return the added object itself. Then, at some point (typically in the controller) you will call UoW.Commit(); and as a result, the Id property of the added entity will contain the updated value.

At this point, you can start using the Id property and for example store it in a cookie as you said.

Dejan
  • 9,150
  • 8
  • 69
  • 117
  • do you mean you would return the Customer object and use that object to take any actions with the ID property before calling commitChanges()? – Computer Aug 06 '21 at 14:53
  • @Computer no. obviously you cannot do any work that depends on the Id to be known before calling `SaveChanges`. – Dejan Aug 08 '21 at 14:05
  • Would be great if you could provide an example so i can take a look? – Computer Aug 10 '21 at 08:17
2

The problem here is that the id is generated by the database, so we need to call SaveChanges so that the database generates the id and EntityFramework will fix the entity up with the generated id.

So what if we could avoid the database roundtrip?

One way to do this is to use a uuid instead of an integer as an id. This way you could simply generate a new uuid in the constructor of your domain model and you could (pretty) safely assume that it would be unique across the entire database.

Of course choosing between a uuid and an integer for the id is an entire discussion of its own: Advantages and disadvantages of GUID / UUID database keys But at least this is one point in favor of a uuid.

Nick Muller
  • 2,003
  • 1
  • 16
  • 43
1

Unit of work should be a transaction for the entire request.

Simply just return the person id from the newly created object.

Depending on what technology you are using for your data access this will differ, but if you are using Entity Framework, you can do the following:

var person = DbContext.Set<Person>().Create();

// Do your property assignment here

DbContext.Set<Person>().Add(person);

return person.Id;

By creating the Person instance this way, you get a tracked instance that allows for lazy loading, and using the Id property, as it will be updated when SaveChanges is called (by ending your unit of work).

  • You should call SaveChanges for getting an updated ID – X.Otano Aug 03 '18 at 12:23
  • As I said in my response, the Id property will be updated once the call to SaveChanges() is made. The Id property will contain a tracked value. – Martin Fletcher Sep 20 '18 at 22:02
  • read my answer please, it's more explained => https://stackoverflow.com/questions/36868254/how-do-i-get-db-generated-ids-back-when-using-repository-pattern/51673207#51673207 – X.Otano Sep 21 '18 at 07:25
1

Instead of IDENTITY, I use SEQUENCE at database level. When a new entity is being created, first, I get the next value of the sequence and use it as Id.

Database:

CREATE SEQUENCE dbo.TestSequence
START WITH 1
INCREMENT BY 1

CREATE TABLE [dbo].[Test](
    [Id]    [int] NOT NULL DEFAULT (NEXT VALUE FOR dbo.TestSequence),
    [Name]  [nvarchar](200) NOT NULL,
    CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED ([Id] ASC)
)

C#:

public enum SequenceName
{
    TestSequence
}

public interface IUnitOfWork : IDisposable
{
    DbSet<TEntity> Set<TEntity>() where TEntity : class;
    void Commit(SqlContextInfo contextInfo);
    int NextSequenceValue(SequenceName sequenceName);
}

public class UnitOfWork : MyDbContext, IUnitOfWork
{
...
    public void Commit(SqlContextInfo contextInfo)
    {
        using (var scope = Database.BeginTransaction())
        {
            SaveChanges();
            scope.Commit();
        }
    }

    public int NextSequenceValue(SequenceName sequenceName)
    {
        var result = new SqlParameter("@result", System.Data.SqlDbType.Int)
        {
            Direction = System.Data.ParameterDirection.Output
        };
        Database.ExecuteSqlCommand($"SELECT @result = (NEXT VALUE FOR [{sequenceName.ToString()}]);", result);
        return (int)result.Value;
    }
...
}

internal class TestRepository
{
    protected readonly IUnitOfWork UnitOfWork;
    private readonly DbSet<Test> _tests;

    public TestRepository(IUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
        _tests = UnitOfWork.Set<Test>();
    }

    public int CreateTestEntity(NewTest test)
    {
        var newTest = new Test
        {
            Id = UnitOfWork.NextSequenceValue(SequenceName.TestSequence),
            Name = test.Name
        };
        _tests.Add(newTest);
        return newTest.Id;
    }
}
Sandor
  • 31
  • 3
1

My solution is returning Lazy<MyModel> by repository method:

    public class MyRepository
    {
       // ----
       public Lazy<MyModel> Insert(MyModel model)
       {
          MyEntity entity = _mapper.MapModel(model);
          _dbContext.Insert(entity);
         return Lazy<MyModel>(()=>_mapper.MapEntity(entity));
       }
 

   }

And in the domain:

Lazy<MyModel> mayModel = unitOfWork.MyRepository.Insert(model);
unitOfWork.Save();
MyModel myModel = myModel.Value;
Morteza
  • 164
  • 1
  • 10
0

I don't think there is a way to do that unless you break the pattern and pass in some extra information about the newly created entity.

Since the Id will only be allocated since commit is successful and if you don't have information about which entities were created/updated/deleted, its almost impossible to know.

I once did it using the code below (I don't recommend it though but I use it for this need specifically)

public string Insert(Person entity)
{
    uow.Repository.Add(entity); //public Repository object in unit of work which might be wrong
    Response responseObject = uow.Save();
    string id = entity.Id;  //gives the newly generated Id
    return id;
}
Ali Baig
  • 3,819
  • 4
  • 34
  • 47
0

To expand on Martin Fletcher his answer:

EF Core generates (depending on the db provider) a temporary value for the generated id, once you add the entity to the DbContext, and start tracking it. So before the unit of work is actually committed via SaveChanges(). After SaveChanges() is called, the DbContext will actually fix up all placeholder ids with the actual generated id.

A nice example (which I quote from this answer):

var x = new MyEntity();        // x.Id = 0
dbContext.Add(x);              // x.Id = -2147482624 <-- EF Core generated id
var y = new MyOtherEntity();   // y.Id = 0
dbContext.Add(y);              // y.Id = -2147482623 <-- EF Core generated id
y.MyEntityId = x.Id;           // y.MyEntityId = -2147482624
dbContext.SaveChangesAsync();
Debug.WriteLine(x.Id);         // 1261 <- EF Core replaced temp id with "real" id
Debug.WriteLine(y.MyEntityId); // 1261 <- reference also adjusted by EF Core

More information:
https://learn.microsoft.com/en-us/ef/core/change-tracking/explicit-tracking#generated-key-values
https://learn.microsoft.com/en-us/ef/core/modeling/generated-properties?tabs=data-annotations#primary-keys

Another option is to generate the keys on the client side (this is possible with for example UUID-based primary keys). But this has been discussed in other answers.

Nick Muller
  • 2,003
  • 1
  • 16
  • 43
  • 2
    I don't see how this answers the question. The problem is decoupling, the entity object is in another layer. – Gert Arnold Sep 03 '21 at 09:57
  • I guess the answer is, when using EF (which I assumed because the user tagged the question), you can simply access the Id once the entity has been added to the Unit of Work (DbContext). The question states: "The problem is that Add and Commit are decoupled, and the Id is not known until after Commit.". This is no longer true in EFCore. The Id can simply be accessed after Add. – Nick Muller Sep 03 '21 at 10:00
  • 1
    That has always been true in EF. They just can't get to the entity object because of architecture. IMO the question can't be answered because there is not a single line of code in it that demonstrates the problem. It should have been closed long ago. – Gert Arnold Sep 03 '21 at 10:08
  • You're right. I guess I (and many other answers here) try to answer a question that's not really asked. Although I think it's a interesting discussion, it's not one that fits here. Now I'm not sure if I should just remove my answer, because it's an alternate approach to other answers here. – Nick Muller Sep 03 '21 at 10:13