I'm using DDD patterns while using ef core and I've a problem to configure an entity to generate a foreign key in this particular scenario.
Suppose a class like this
public class GasStation : Entity, IAggregateRoot, IPlace
{
public ImportFeed ImportFeed { get; private set; }
...
where ImportFeed is a special class similar to an Enum
public class ImportFeed : Enumeration
{
public static ImportFeed None = new(1, nameof(None), "No feed.");
public static ImportFeed ItalianMiseGasStations = new(2, nameof(ItalianMiseGasStations), "Italian Gas Stations");
public static ImportFeed ItalianMiseGasStationPrices = new(3, nameof(ItalianMiseGasStationPrices), "Italian Gas Stations Prices");
/// <summary>
/// The description of the import feed.
/// </summary>
public string? Description
{
get;
private set;
}
public ImportFeed(int id, string name, string description) : base(id, name)
{
Description = description;
}
}
Enumeration class is similar to enum as concept, basically I never load such entities from DB but define them as static properties but they are anyway stored on Db to maintain relationships between tables.
public abstract class Enumeration : IComparable
{
public int Id { get; private set; }
public string Name { get; private set; }
protected Enumeration(int id, string name)
{
Id = id > 0 ? id : throw new DomainException(nameof(id));
Name = string.IsNullOrWhiteSpace(name) ? throw new DomainException(nameof(name)) : name;
}
public override string ToString() => Name;
public static IEnumerable<T> GetAll<T>() where T : Enumeration
{
var fields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly);
return fields.Select(f => f.GetValue(null)).Cast<T>();
}
public override bool Equals(object? obj)
{
if (obj is not Enumeration otherValue) return false;
var typeMatches = GetType().Equals(obj.GetType());
var valueMatches = Id.Equals(otherValue.Id);
return typeMatches && valueMatches;
}
public override int GetHashCode() => Id.GetHashCode();
public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
{
var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id);
return absoluteDifference;
}
public static bool IsValid<T>(int value) where T : Enumeration
=> Parse<T, int>(value, "value", item => item.Id == value, false) is not null;
public static T FromValue<T>(int value) where T : Enumeration
=> Parse<T, int>(value, "value", item => item.Id == value)!;
public static T FromDisplayName<T>(string displayName) where T : Enumeration
=> Parse<T, string>(displayName, "display name", item => item.Name == displayName)!;
private static T? Parse<T, K>(K value, string description, Func<T, bool> predicate, bool raiseError = true) where T : Enumeration
{
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
if (raiseError && matchingItem == null)
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
return matchingItem;
}
public int CompareTo(object? other) => Id.CompareTo(((Enumeration?)other)?.Id);
}
My entity configuration is something like this:
class GasStationEntityTypeConfiguration : BaseEntityTypeConfiguration<GasStation>
{
public override string TableName => "GasStations";
public override void ConfigureColumns(EntityTypeBuilder<GasStation> config)
{
config.HasIndex(b => new { b.ImportFeed, b.ImportFeedEntryId });
config.Property(b => b.Name)
.IsRequired();
config.Property(b => b.Location)
.IsRequired()
.HasColumnName("Location")
.HasConversion(new GeoPointConverter());
config.Property(b => b.ImportFeed)
.IsRequired()
.HasColumnName("ImportFeedId")
.HasConversion(b => b.Id, b => Enumeration.FromValue<ImportFeed>(b));
config.HasOne(b => b.ImportFeed)
.WithMany()
.HasForeignKey(b => b.ImportFeed)
.IsRequired();
}
}
base class just define the Id mapping and hide some properties that doesn't have to be mapped.
Now as you see I have a custom converter for the ImportFeed property because I don't want to load it from db and instead I just store the "enum id" and doing so I specify that the ColumnName for the ImportFeed property is ImportFeedId.
What I want to do is to create a foreing key to the ImportFeeds table but with the code above, it generates the error
The property or navigation 'ImportFeed' cannot be added to the entity type 'GasStation' because a property or navigation with the same name already exists on entity type 'GasStation'.
I tried different combinations but it keeps generating this problem, the only way to avoid that is to change
config.HasOne(b => b.ImportFeed)
.WithMany()
.HasForeignKey(b => b.ImportFeed)
.IsRequired();
to
config.HasOne<ImportFeed>()
.WithMany()
.HasForeignKey("ImportFeedId")
.IsRequired();
like I tried to do as first because I don't need a navigation property for that foreign key since I've already one. The problem is that this snippets generates a new column ImportFeedId1 and create a foreign key on that...
How am I supposed to create a foreign key using code-first (without attributes) in this scenario? Is this something not supported?
I know I can manually add it in a custom migration but of course I don't want to do that Thanks.
