Hi,
I wonder if is possible to use value objects (complex types) as entity ID in EF Core.
Something like this:
public class Book
{
private BookId bookId;
public Book(BookId bookId)
{
}
}
public class BookId
{
private readonly int id;
public BookId(int id)
{
this.id = id;
}
}
Regards!
@AndriySvyryd Can you comment on this?
@lurumad No, currently any constraints defined on an entity type (keys, foreign keys, indexes) can only use the properties defined on the type (or inherited).
Does this limitation block your scenario? If so, could you elaborate on why the ID needs to be on a separate type?
Hi @AndriySvyryd
One of the main DDD principles when modeling a domain is SoC (Separation of Concerns). We should isolate our domain from infrastructure, This kind of Id's are related with persistence logic, in this case is related with EF Core. Imagine that I decided to change my single key of book to a composite key. In my example the meaning of the code would remain the same.
Regards!
@lurumad, based on the SoC principle, EF entities should not be part of the domain model. They are part of a data access and part of a repository at best.
You are totally right @popcatalin81 but I would like to avoid the mapping between the domain model and persistence model, yes I know it's not the right way, but sometimes the cost is expensive and sometimes I would to avoid separate models, but anyway, you are rigth ;)
Regards!
@lurumad You can still get most of the benefits if you just keep the Id field private and don't map BookId
.
```C#
public class Book
{
private int _id;
private BookId BookId
{
get => new BookId(_id);
set => _id = value.Id;
}
private Book()
{
}
public Book(BookId bookId)
{
BookId = bookId;
}
}
public class BookId
{
public BookId(int id)
{
Id = id;
}
public int Id { get; set; }
}
```
@AndriySvyryd that makes certain queries allot harder to write. It's not ideal.
@popcatalin81. With code first approach we could use POCO types as domain model classes and EF Core is a step in the right direction but not ideal either.
@lurumad As of EF Core 2.1, this is now supported using value converters. However, in this case there is currently a limitation that this will not also work out-of-the-box with store generated keys. See #11597
@lurumad may be it help you to use all of power of ddd.
` public static class PropertyBuilderExtensions
{
public static PropertyBuilder
Expression
Expression
IServiceProvider serviceProvider,
string sequenceName,
string sequenceSchema = null)
{
var valueConverter =
new ObjectIdValueConverter
propertyBuilder
.HasConversion(valueConverter)
.ValueGeneratedOnAdd()
.HasColumnName("Id")
.IsRequired();
propertyBuilder.Metadata.SqlServer().HiLoSequenceName = sequenceName;
propertyBuilder.Metadata.SqlServer().HiLoSequenceSchema = sequenceSchema;
return propertyBuilder;
}
}
public class ObjectIdValueConverter
{
private readonly ISqlServerValueGeneratorCache _sqlServerValueGeneratorCache;
private readonly ISqlServerConnection _sqlServerConnection;
private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder;
private readonly ISqlServerUpdateSqlGenerator _sqlServerUpdateSqlGenerator;
public ObjectIdValueConverter(Expression<Func<TModel, TProvider>> convertToProviderExpression, Expression<Func<TProvider, TModel>> convertFromProviderExpression, IServiceProvider serviceProvider) : base(convertToProviderExpression, convertFromProviderExpression)
{
_sqlServerValueGeneratorCache = serviceProvider.GetService<ISqlServerValueGeneratorCache>();
_sqlServerConnection = serviceProvider.GetService<ISqlServerConnection>();
_rawSqlCommandBuilder = serviceProvider.GetService<IRawSqlCommandBuilder>();
_sqlServerUpdateSqlGenerator = serviceProvider.GetService<ISqlServerUpdateSqlGenerator>();
}
public override ConverterMappingHints MappingHints => new ConverterMappingHints(
valueGeneratorFactory: (property, type) =>
{
property
.AsProperty()
.SetAnnotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.SequenceHiLo);
var factory = new SqlServerSequenceHiLoObjectIdValueGenerator<TModel>(
() => property.GetValueConverter().ConvertFromProvider,
_rawSqlCommandBuilder
, _sqlServerUpdateSqlGenerator
, _sqlServerValueGeneratorCache.GetOrAddSequenceState(property)
, _sqlServerConnection);
return factory;
});
}
public class SqlServerSequenceHiLoObjectIdValueGenerator<TEntity> : SqlServerSequenceHiLoValueGenerator<TEntity>
{
private readonly Func<object, object> _convertFromProvider;
private readonly HiLoValueGeneratorState _generatorState;
public SqlServerSequenceHiLoObjectIdValueGenerator(Func<Func<object, object>> convertFromProvider,
IRawSqlCommandBuilder rawSqlCommandBuilder, ISqlServerUpdateSqlGenerator sqlGenerator,
SqlServerSequenceValueGeneratorState generatorState, ISqlServerConnection connection)
: base(rawSqlCommandBuilder, sqlGenerator, generatorState,connection)
{
_convertFromProvider = convertFromProvider();
_generatorState = generatorState;
}
public override TEntity Next(EntityEntry entry)
{
var value = _generatorState.Next<long>(base.GetNewLowValue);
var converted = _convertFromProvider(value);
return (TEntity) converted;
}
public override async Task<TEntity> NextAsync(EntityEntry entry, CancellationToken cancellationToken = default)
{
var value = await _generatorState.NextAsync<long>(base.GetNewLowValueAsync,cancellationToken);
var converted = _convertFromProvider(value);
return (TEntity) converted;
}
}
`
and how use it
`
public class User
{
public User()
{
}
public UserId Id { get; set; }
public Credentials Credentials { get; set; }
}
public class UserId
{
public UserId()
{
}
public UserId(long id)
{
Id = id;
}
public long Id { get; private set; }
public static implicit operator long(UserId beaconId)
{
return beaconId.Id;
}
public static implicit operator UserId(Int64 id)
{
return new UserId(id);
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var userId = (UserId) obj;
return this.Id == userId.Id;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence("Default", builder => { builder.IncrementsBy(1); });
var userModelBuilder = modelBuilder.Entity<User>();
userModelBuilder.Property(x => x.Id)
.UseSqlServerObjectIdGenerator(x => x.Id, l => l, this.GetInfrastructure(), "Default");
userModelBuilder.OwnsOne(x => x.Credentials, c =>
{
c.Property(x => x.Email);
c.Property(x => x.Password);
});
}
`
Most helpful comment
Hi @AndriySvyryd
One of the main DDD principles when modeling a domain is SoC (Separation of Concerns). We should isolate our domain from infrastructure, This kind of Id's are related with persistence logic, in this case is related with EF Core. Imagine that I decided to change my single key of book to a composite key. In my example the meaning of the code would remain the same.
Regards!