Closed ComptonAlvaro closed 1 year ago
@ComptonAlvaro Your description is a bit vague, but here's one approach:
public static class Your
{
public static string ConnectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow";
}
public class Order
{
public int Id { get; set; }
public OrderState State { get; set; } = null!;
public void ToAccepted() => State = new AcceptedState();
public void ToCreated() => State = new CreatedState();
}
public class OrderStateDescription
{
public OrderState Id { get; set; } = null!;
public string Description { get; set; } = null!;
}
public abstract class OrderState
{
protected abstract int Id { get; }
protected bool Equals(OrderState other)
=> Id == other.Id;
public override bool Equals(object? obj)
=> !ReferenceEquals(null, obj)
&& (ReferenceEquals(this, obj)
|| obj.GetType() == GetType()
&& Equals((OrderState)obj));
public override int GetHashCode() => Id;
public class OrderStateConverter : ValueConverter<OrderState, int>
{
public OrderStateConverter()
: base(v => v.Id, v => Factory(v))
{
}
private static OrderState Factory(int id)
=> id == CreatedState.StateId
? new CreatedState()
: id == AcceptedState.StateId
? new AcceptedState()
: throw new ArgumentOutOfRangeException();
}
}
public class CreatedState : OrderState
{
public static int StateId => 1;
protected override int Id => StateId;
}
public class AcceptedState : OrderState
{
public static int StateId => 2;
protected override int Id => StateId;
}
public class SomeDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(Your.ConnectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(b =>
{
b.Property(e => e.State).HasConversion<OrderState.OrderStateConverter>();
b.HasOne<OrderStateDescription>()
.WithMany()
.HasForeignKey(e => e.State);
});
modelBuilder.Entity<OrderStateDescription>(b =>
{
b.Property(e => e.Id).HasConversion<OrderState.OrderStateConverter>();
b.HasData(
new OrderStateDescription {Id = new AcceptedState(), Description = "Accepted"},
new OrderStateDescription {Id = new CreatedState(), Description = "Created"});
});
}
}
public class Program
{
public static void Main()
{
using (var context = new SomeDbContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var order = new Order();
order.ToCreated();
context.Add(order);
context.SaveChanges();
}
using (var context = new SomeDbContext())
{
var order = context.Orders.First();
order.ToAccepted();
context.SaveChanges();
}
}
}
Thanks for the solution.
It is more or less what I need, but in my case OrderState wouldn't have an Id, because it is an object model, so I guess in the configuration of the mdel in EF Core, I would need to set a shadow property for the Id, so I will have this field in the model but I will not have in the class of the domain.
Also, the converter it is defined as subclass in the OrderState class, that is a domain class, so I would prefer to don't have in this class, because it is not a concern of the domain, but it is a concern of EF Core. But I guess that perhaps I could define this converter outside this class and create it in the repository.
So in sumary, it is good point to start but I would like two things:
1.- Don't have an Id for the order state. It is an object model. 2.- Don't define the converter in the domain class, so I would prefer to define it in some where in the repository.
@ComptonAlvaro
For 1, since you want to have an ID in the database there will need to be a mapping somewhere. You can separate it out from the domain type, but how to get from the domain object to an ID must exist somewhere unless you also remove it from the database.
For 2, It can go anywhere you want, but it will need access to the object to ID mapping.
Personally, I'd do it like this if I had that database schema:
public class Order
{
public int Id { get; set; }
public OrderState State { get; private set; }
public void ToAccepted() => State = OrderState.Accepted;
public void ToCreated() => State = OrderState.Created;
}
public enum OrderState
{
Accepted = 1,
Created = 2
}
public class OrderStateDescription
{
public OrderState Id { get; set; }
public string Description { get; set; } = null!;
}
public class SomeDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(Your.ConnectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(b =>
{
b.HasOne<OrderStateDescription>()
.WithMany()
.HasForeignKey(e => e.State);
});
modelBuilder.Entity<OrderStateDescription>(b =>
{
b.HasData(
new OrderStateDescription {Id = OrderState.Accepted, Description = nameof(OrderState.Accepted)},
new OrderStateDescription {Id = OrderState.Created, Description = nameof(OrderState.Created)});
});
}
}
public class Program
{
public static void Main()
{
using (var context = new SomeDbContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var order = new Order();
order.ToCreated();
context.Add(order);
context.SaveChanges();
}
using (var context = new SomeDbContext())
{
var order = context.Orders.First();
order.ToAccepted();
context.SaveChanges();
}
}
}
It's simpler, faster, less code, less to go wrong, and easier to maintain.
@ajcvickers Thanks.
Yes it is the way in which i was thinking, and for me the code is more clear in this way.
Thank you so much.
Supose that I have in my domain a root entity that is Order and many values objects for the state, one for each possible state (CreatedState, AcceptedState...). So OrderStatus has not an ID.
However, in my database, I have a table for the status, OrderStatus, that have and ID and a varchar for the description (Created, Accepted...).
But I don't know how to map or how to map this case with fluent API and how to work in the domain.
Supose that I want to pass an order from Created to acepted. I would do this in my model:
In my application service I call the repository and the domain logic to do the action:
But how should to map the Order and OrderStatus domain entities in the repository with fluent API? Because in some why entity core should to now that the Accepted state has for example ID = 1 in the database to can update the Order.StateId field in the database to this value.
In sumary, I would like to know how could I update the foreign key of the status in the database when in the domain the state is a value object without an ID. Is it possible?
Thanks.