Entity Framework Core 5.0.3 - One To One mapping with shadow property foreign keys - entity-framework-core

For the life of me, I cannot get this to work properly. I have a relatively simple domain model that has a couple of navigation properties that I want to fill out via eager loading.
To keep my domain model pure, I have opted to use shadow properties as foreign keys, so they are not accessible by the client code.
This is the domain model:
public class CourseType : Entity
{
protected CourseType() { }
public CourseType(string name, CoachGroup coachGroup, bool active)
{
Name = name;
CoachGroup = coachGroup;
Active = active;
}
public string Name { get; private set; }
private int? _coachGroupId;
private int? CoachGroupId => _coachGroupId;
public virtual CoachGroup CoachGroup { get; private set; }
public int? AgeLimit { get; private set; }
public bool Active { get; private set; }
public bool ShowInSearchForm { get; private set; }
private List<Course> _accessGivingCourses = new List<Course>();
public virtual IReadOnlyList<Course> AccessGivingCourses => _accessGivingCourses?.ToList();
}
This is how I wire the configuration up:
public class CourseTypeConfiguration : IEntityTypeConfiguration<CourseType>
{
public void Configure(EntityTypeBuilder<CourseType> builder)
{
builder.ToTable("CourseTypes", "Courses");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).HasColumnName("CourseTypeId");
builder.Property(p => p.Name).HasColumnName("CourseTypeName");
builder.Property(p => p.Active).HasColumnName("IsActive");
builder.Property(p => p.ShowInSearchForm).HasColumnName("ShowInSearchForm");
builder.Property(p => p.AgeLimit).HasColumnName("AgeLimit");
builder.Property<int?>("CoachGroupId").HasField("_coachGroupId");
builder.HasOne(p => p.CoachGroup).WithOne().HasForeignKey<CourseType>("CoachGroupId").OnDelete(DeleteBehavior.Restrict);
builder.HasMany(p => p.AccessGivingCourses).WithMany("AccessGivingCourses")
.UsingEntity<Dictionary<string, object>>("CourseTypesAccessGivingCourses",
j => j.HasOne<Course>().WithMany().HasForeignKey("CourseId"),
j => j.HasOne<CourseType>().WithMany().HasForeignKey("CourseTypeId"),
j => j.ToTable("CourseTypesAccessGivingCourses")
);
builder.HasIndex("CoachGroupId").IsUnique(false);
}
}
This is how I extract the data via my repository class:
public override async Task<IEnumerable<CourseType>> GetAll()
{
try
{
return await Context.CourseTypes.Include(i => i.CoachGroup).Include(i => i.AccessGivingCourses).ToListAsync();
}
catch (Exception e)
{
Logger.LogCritical(e, $"Could not retrieve list of course type entities");
throw;
}
}
It ALMOST works, except for the fact that when I add or update entities, the CoachGroup link randomly gets lost for some updates. For others, it works just fine. It's like Entity Framework Core OR the database randomly loses track of it. Which is odd, because when I look in the database table, the foreign keys in the table are all there like they're supposed to!?
Does anyone have any idea what the hell I am doing wrong? Or if this is the correct approach to this problem at all? All I want to do is to load related data, but it's getting to the point where it's becoming rocket science to merely link a couple of optional relationships together...

Related

How do you map subclass properties to columns in Table per Hierarchy?

I have a TPH situation where I have an abstract base class and 8 derived classes from it by using a discriminator. Two of them share a list of sub classes.
public abstract class StepBase : FullAuditedEntity<Guid>
{
public int Order { get; set; }
public StepType StepType { get; set; }
}
The thing is I have two types which shares a SubClass
public class DestinationVesselStep : StepBase
{
public virtual List<DestinationVessel> VesselsDestination { get; set; }
}
public class LiquidNitrogenStep : StepBase
{
public virtual List<DestinationVessel> DestinationsBoxes { get; set; }
}
private static void ConfigureVesselsStep(ModelBuilder builder)
{
builder.Entity<DestinationVesselStep>(b =>
{
//Properties
b.HasMany(p => p.VesselsDestination).WithOne().HasForeignKey(x => x.StepId);
});
}
private static void ConfigureLiquidNitrogenStep(ModelBuilder builder)
{
builder.Entity<LiquidNitrogenStep>(b =>
{
//Properties
b.HasMany(p => p.DestinationsBoxes).WithOne().HasForeignKey(x => x.StepId);
});
}
But when I request a LiquidNitrogenStep with two or more destinationBoxes I get the following error:
System.InvalidOperationException : Sequence contains more than one element.
it works fine if I only have one destinationBox
I am expecting to get a LiquidNitrogenStep with all its destination boxes, the error do not happnd with DestinationVesselStep
DestinationVessel.StepId can't refer to both a DestinationVesselStep and LiquidNitrogenStep.
So either add separate foreign keys to DestinationVessel, eg LiquidNitrogenStepId, and DestinationVesselStepId, or make the relationships many-to-many, which uses separate linking tables for each relationship, instead of putting foreign keys on the target Entity.
private static void ConfigureVesselsStep(ModelBuilder builder)
{
builder.Entity<DestinationVesselStep>(b =>
{
//Properties
b.HasMany(p => p.VesselsDestination).WithMany( d => d.DestinationSteps);
});
}
private static void ConfigureLiquidNitrogenStep(ModelBuilder builder)
{
builder.Entity<LiquidNitrogenStep>(b =>
{
//Properties
b.HasMany(p => p.DestinationsBoxes).WithMany(d => d.LiquidNitrogenSteps);
});
}

Entity Framework Core (6.0.8) indirect Many to Many relationship with 2 db contexts

I am unable to generate a migration for a many to many relationship that spans 2 different db contexts.
I have 3 entities:
AccountApp, SubscriptionDetail, SubscribedAccountApp
AccountApp and SubscribedAccountApp are the AccountDbContext while the SubscriptionDetail is in a SubscriptionsDbContext.
I want to set up an indirect many to many relationship as described here
Based on the documentation, I have created an EntityTypeConfig for the SubscribedAccountApp like this
public class SubscribedAccountAppConfig : IEntityTypeConfiguration<SubscribedAccountApp>
{
public void Configure(EntityTypeBuilder<SubscribedAccountApp> builder)
{
builder.ToTable("subscribed_account_apps");
builder.HasKey(m => new { m.SubscriptionDetailId, m.AccountAppId });
builder.HasOne(x => x.SubscriptionDetail)
.WithMany(x => x.SubscribedAccountApps)
.HasForeignKey(x => x.SubscriptionDetailId);
builder.HasOne(x => x.AccountApp)
.WithMany(x => x.SubscribedAccountApps)
.HasForeignKey(x => x.AccountAppId);
}
}
This config is then applied to the AccountDbContext like so
public class AccountDbContext : DbContext
{
public IQueryable<AccountApp> AccountApps { get; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new AccountAppConfig());
modelBuilder.ApplyConfiguration(new SubscribedAccountAppConfig());
}
}
The data model for SubsriptionsDetails looks like:
public class SubscriptionDetail
{
private readonly List<SubscribedAccountApp> _subscribedAccountApps;
private readonly List<ConsumptionComponent> _consumptionComponents;
public SubscriptionDetail(
Guid id,
IEnumerable<ConsumptionComponent> consumptionComponents = null,
IEnumerable<SubscribedAccountApp> subscribedAccountApps = null)
{
Id = id;
_consumptionComponents = consumptionComponents ?? new List<ConsumptionComponent>();
_subscribedAccountApps = subscribedAccountApps ?? new List<SubscribedAccountApp>();
}
public Guid Id { get; set; }
public IReadOnlyCollection<ConsumptionComponent> ConsumptionComponents => _consumptionComponents;
public IReadOnlyCollection<SubscribedAccountApp> SubscribedAccountApps => _subscribedAccountApps;
}
The data model for AccountApp looks like:
public class AccountApp
{
public readonly List<SubscribedAccountApp> _subscribedAccountApps;
public AccountApp(Guid id,
Guid accountId,
IEnumerable<SubscribedAccountApp> subscribedAccountApps = null)
{
Id = id;
_subscribedAccountApps = subscribedAccountApps.ToNavigationProperty();
}
public Guid Id { get; private set; }
public IReadOnlyCollection<SubscribedAccountApp> SubscribedAccountApps => _subscribedAccountApps;
}
This leaves me an error when I try to create a migration for the AccountDbContext:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
System.InvalidOperationException: No suitable constructor was found for entity type 'ConsumptionComponent'.
This error describes a ConsumptionComponent constructor for some reason, although this model has already existed and nothing has changed on that model. This model does appear as an IEnumerable in the SubscriptionDetail constructor but I am not sure why it is showing up as an error. All I want this migration to do is add support for the indirect-many-to-many-relationship which has nothing to do with ConsumptionComponent.

Is it possible to add foreign key between owned entities in EF Core 6

I am trying to separate my contexts using DDD. I have two domains, Instruments and Advertisements with its aggregate roots (the example is hypothetical). Instrument AR owns many InstrumentPictures and I would like to have that information in the Advertisement domain as well via proxy entity.
To ensure good database integrity it would be better to create foreign key from AdvertisementPicture.Guid to InstrumentPicture.Guid but as far as I know this can be done only through HasOne/HasMany model configuration.
Am I using the owner relationship wrong?
(Note: I do not want to configure the FK with custom sql migration.)
Instrument AR:
public class Instrument
{
protected Instrument()
{
}
public Instrument(string name, IEnumerable<InstrumentPicture> pictures)
{
Name = name;
_instrumentPictures.AddRange(pictures);
}
protected List<InstrumentPicture> _instrumentPictures = new List<InstrumentPicture>();
public IReadOnlyCollection<InstrumentPicture> InstrumentPictures
=> _instrumentPictures.AsReadOnly();
public Guid Guid { get; private set; }
public string Name { get; private set; }
}
InstrumentPicture owned collection:
public class InstrumentPicture
{
protected InstrumentPicture()
{
}
public InstrumentPicture(Guid guid, string url)
{
Guid = guid;
Url = url;
}
public Guid Guid { get; set; }
public string Url { get; set; }
public DateTime Created { get; set; }
}
Advertisiment AR
public class Advertisement
{
protected Advertisement()
{
}
public Advertisement(Guid instrumentGuid, string name, IEnumerable<AdvertisementPicture> pictures)
{
InstrumentGuid = instrumentGuid;
Name = name;
_advertisementPictures.AddRange(pictures);
}
protected List<AdvertisementPicture> _advertisementPictures = new List<AdvertisementPicture>();
public IReadOnlyCollection<AdvertisementPicture> AdvertisementPictures
=> _advertisementPictures.AsReadOnly();
public Guid Guid { get; private set; }
public Guid InstrumentGuid { get; private set; }
public string Name { get; private set; }
}
AdvertisementPicture proxy
public class AdvertisementPicture
{
protected AdvertisementPicture()
{
}
public AdvertisementPicture(Guid guid, string url)
{
Guid = guid;
Url = url;
}
public Guid Guid { get; set; }
public string Url { get; set; }
}
Model configuration:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Instrument>()
.HasKey(e => e.Guid);
modelBuilder.Entity<Instrument>()
.OwnsMany(e => e.InstrumentPictures, pic =>
{
pic.HasKey(e => e.Guid);
});
modelBuilder.Entity<Advertisement>()
.HasKey(e => e.Guid);
modelBuilder.Entity<Advertisement>()
.HasOne<Instrument>()
.WithMany()
.HasForeignKey(e => e.InstrumentGuid);
modelBuilder.Entity<Advertisement>()
.OwnsMany(e => e.AdvertisementPictures, pic =>
{
pic.HasKey(e => e.Guid);
// How can I add a foreign key to original InstrumentPicture for database integrity?
});
}
I've been struggling with this for hours and finding lots of answers on SO saying this isn't possible. Turns out this is possible using EntityFrameworkCore so I'll post what I've found on my Top Google Search for this problem.
As soon as you add a foreign key you will find the migration tool attempting to create the table in the second DBContext (unless you add ModelBuilder.Ignore<>() which will either do nothing or ignore your foreign key depending on your order of operations).
You can however do something like this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUser>()
.ToTable("AspNetUsers", t => t.ExcludeFromMigrations());
}
This will allow you to reference tables in other DBContext's but exclude any changes to them from the one you're working in. This is outlined in the MS documentation here.
If you have used Fluent API you may still need to apply those configurations in the referencing DB Context. This is easily achieved if you have used the IEntityTypeConfiguration<T> implementation by an additional call to ModelBuilder.ApplyConfigurationsFromAssembly(typeof(T).Assembly);.
In such a use case as above you may find yourself excluding a lot of different entities from your DB context. If you have these defined in their own library like I have to follow a DDD pattern you may find an extension method useful to exclude all of them at once:
public static class ExcludeEntitiesInAssemblyFromMigrationsExtension
{
public static void ExcludeEntitiesInAssemblyFromMigrations(this ModelBuilder builder, Assembly assembly)
{
var assemblyTypes = assembly.GetExportedTypes().Where(t => t.IsClass && !t.IsAbstract);
foreach (var assemblyType in assemblyTypes)
{
var entityBuilder = builder.Entity(assemblyType);
var entityTablename = entityBuilder.Metadata.GetTableName();
if (entityTablename != null)
{
entityBuilder.ToTable(entityTablename, t => t.ExcludeFromMigrations());
}
}
}
}

Using DTO with OData in .NetCore 2.1

I am writing a test OData Rest API with an InMemoryDatabase.
I would like to use DTO(s) to hide the SQL model and adjust a few fields (geographic positions and so on).
However, when I use ProjectTo<...> method from AutoMapper, GET request to the API return an empty collection instead of the actual result list.
Do you have any idea about what I am doing wrong ?
Here is the controller :
namespace offers_api.Controllers
{
public class OffersController : ODataController
{
private readonly OfferContext _context;
private IMapper _mapper;
public OffersController(OfferContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
[EnableQuery]
public IActionResult Get()
{
IQueryable<Offer> res = _context.Offers.ProjectTo<Offer>(_mapper.ConfigurationProvider); // <-- works without ProjectTo !
return Ok(res);
}
}
}
The automapper declaration :
namespace offers_api.Entities
{
public class Mapping : Profile
{
public Mapping()
{
//CreateMap<CategoryEntity, string>().ConvertUsing(cat => cat.Name ?? string.Empty);
CreateMap<LocationEntity, Location>()
.ForMember(x => x.longitude, opt => opt.MapFrom(o => 0))
.ForMember(x => x.latitude, opt => opt.MapFrom(o => 0))
.ReverseMap();
CreateMap<OfferEntity, Offer>()
.ForMember(x => x.Category, opt => opt.MapFrom(o => o.Category.Name))
.ReverseMap()
.ForMember(x => x.Category, opt => opt.MapFrom(o => new CategoryEntity { Name = o.Category }));
CreateMap<OfferPictureEntity, OfferPicture>().ReverseMap();
CreateMap<UserEntity, User>().ReverseMap();
}
}
}
The EDM model :
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Offer>("Offers");
return builder.GetEdmModel();
}
I found the solution.
In fact, automapper loaded more data than OData's default behaviour.
The relation between an offer and it's author was described by a non nullable foreing key. I didn't insert any author in the DB, but OData tried to load a user and saw it was missing in the USER table, so it discarded the Offer result.
Solution : make the foreign key nullable.
namespace offers_api.Entities
{
public class OfferEntity
{
[Key]
public long Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public long AuthorId { get; set; } // <== Bug here : add long? to resolve it...
public virtual UserEntity Author { get; set; }
}
}

InvalidOperationException: The entity type 'Enrollments' requires a primary key to be defined

I am new to Asp.Net Core (Even to Asp.Net and web). I am using Asp.Net Core 2 with MySQL, using Pomelo.EntityFrameWorkCore.MySql (2.0.1) driver. I just created a custom dbcontext with Courses and Enrollments table, along with the default created ApplicationDbContext. The Primary Key for Enrollments is a composite key, comprising of UserId and CourseId. Below is the code :
public class CustomDbContext : DbContext
{
public virtual DbSet<Courses> Courses { get; set; }
public virtual DbSet<Enrollments> Enrollments { get; set; }
public CustomDbContext(DbContextOptions<CustomDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Courses>(entity =>
{
entity.ToTable("courses");
entity.HasIndex(e => e.Name)
.HasName("Coursescol_UNIQUE")
.IsUnique();
entity.Property(e => e.Id).HasColumnType("int(11)");
entity.Property(e => e.Duration).HasColumnType("time");
entity.Property(e => e.Name).HasMaxLength(45);
});
modelBuilder.Entity<Enrollments>(entity =>
{
entity.HasKey(e => new { e.UserId, e.CourseId });
entity.ToTable("enrollments");
entity.HasIndex(e => e.CourseId)
.HasName("fk_Courses_Enrollments_CourseId_idx");
entity.HasIndex(e => e.UserId)
.HasName("fk_Users_Enrollments_CourseId_idx");
entity.HasIndex(e => new { e.UserId, e.CourseId })
.HasName("UniqueEnrollment")
.IsUnique();
entity.Property(e => e.CourseId).HasColumnType("int(11)");
entity.HasOne(d => d.Course)
.WithMany(p => p.Enrollments)
.HasForeignKey(d => d.CourseId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("fk_Courses_Enrollments_CourseId");
entity.HasOne(d => d.User)
.WithMany(p => p.Enrollments)
.HasForeignKey(d => d.UserId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("fk_Users_Enrollments_UserId");
});
}
}
The Program.cs goes like :
public static void Main(string[] args)
{
var host = BuildWebHost(args);
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<CustomDbContext>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
The configure services method in Startup.cs goes like :
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
services.AddDbContext<CustomDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
}
The Courses Model goes like :
public partial class Courses
{
public Courses()
{
Enrollments = new HashSet<Enrollments>();
}
public int Id { get; set; }
public string Name { get; set; }
public TimeSpan? Duration { get; set; }
public ICollection<Enrollments> Enrollments { get; set; }
}
The Enrollments Model goes like :
public partial class Enrollments
{
public string UserId { get; set; }
public int CourseId { get; set; }
public Courses Course { get; set; }
public ApplicationUser User { get; set; }
}
The applicationUser model goes like :
public ApplicationUser()
{
Enrollments = new HashSet<Enrollments>();
}
public ICollection<Enrollments> Enrollments { get; set; }
Now, here's what I've tried so far :
If i add Course and Enrollment model to the ApplicationDBContext, then everything goes fine.
If in CustomDBContext i have a non-composite primary Key, even then it works fine. (I just tried another example)
Can somebody please throw some light on why is this error ? Is this the intended way to handle such a case ?
Thanks in advance.
It's because the Enrollments entity has been discovered by ApplicationDbContext through ApplicationUser.Enrollments navigation property. This is explained in the Including & Excluding Types - Conventions section of the EF Core documentation:
By convention, types that are exposed in DbSet properties on your context are included in your model. In addition, types that are mentioned in the OnModelCreating method are also included. Finally, any types that are found by recursively exploring the navigation properties of discovered types are also included in the model.
I guess now you see the problem. The Enrollments is discovered and included in the ApplicationDbContext, but there is no fluent configuration for that entity there, so EF uses only the default conventions and data annotations. And of course composite PK requires fluent configuration. And even there wasn't a composite PK, it's still incorrect to ignore the existing fluent configuration. Note that Courses is also included in the ApplicationDbContext by the aforementioned recursive process (through Enrollments.Courses navigation property). Etc. for other referenced classes.
Note that the same applies in the other direction. ApplicationUser and all referenced from it are discovered and included in the CustomDbContext w/o their fluent configuration.
The conclusion - don't use separate contexts containing interrelated entities. In your case, put all the entities in the ApplicationDBContext.