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?
EF execute each INSERT command followed by SELECT scope_identity() statement. SCOPE_IDENTITY returns the last identity value inserted into an identity column in the same scope. The above example will execute the following SQL in the database. WHERE @@ROWCOUNT = 1 AND [StudentID] = scope_identity();
Unit of Work is the concept related to the effective implementation of the repository pattern. non-generic repository pattern, generic repository pattern. Unit of Work is referred to as a single transaction that involves multiple operations of insert/update/delete and so on.
By taking advantage of dependency injection (DI), repositories can be injected into a controller's constructor. the following diagram shows the relationship between the repository and Entity Framework data context, in which MVC controllers interact with the repository rather than directly with Entity Framework.
The unit of work class serves one purpose: to make sure that when you use multiple repositories, they share a single database context. That way, when a unit of work is complete you can call the SaveChanges method on that instance of the context and be assured that all related changes will be coordinated.
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.
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
}
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.
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).
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With