问题
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?
回答1:
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
}
回答2:
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.
回答3:
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;
}
回答4:
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).
回答5:
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;
}
}
来源:https://stackoverflow.com/questions/41862779/how-to-get-id-from-add-using-unitofwork-pattern