asp.net-coreentity-framework-coreef-code-first

EF Core - Need to reference the many-to-many join table in code-first application


I have a mapping application that will display pins on a map that represent Features at Member addresses (e.g. a Member lives at lat/long 'X' and is associated to Feature 'Y'). Members can be associated with multiple Features and a Feature can be associated with multiple Members (many-to-many). In my ASP.NET Core/EF Core app, I created (code-first) the database and EF created a FeatureMember table to manage the Many-To-Many relationship using the MemberId and FeatureId columns from the Member and Feature tables. All works fine up to this point - the app handles the Many-To-Many relationship perfectly.

Now, on to the mapping issue I have. On the map display page, I present a multi-select list of Features to the user. When the user updates the map, JavaScript captures an array of selected FeatureId's (hidden fields) and then an Ajax call sends the array back to the Controller and Repository classes. All is good until I try to build the query to get the required mapping information from the database. The query I built in SSMS (that works fine) joins to the FeatureMember table which EF Core does not recognize as an Entity.

The Error Message from the compiler is: CS1061: 'MappingContext' does not contain a definition for 'FeatureMember' and no accessible extension method 'FeatureMember' accepting a first argument of type 'MappingContext' could be found (are you missing a using directive or an assembly reference?)

The SQL that works in SSMS is:

SELECT
    m.MemberMailingName,a.AddressStreetNumber,a.AddressStreetName,a.AddressCity,a.AddressState,a.AddressZip,g.Latitude,g.Longitude,f.FeatureName
FROM
    Member m
JOIN MailAddress a ON m.MemberId = a.MemberId
JOIN FeatureMember fm ON m.MemberId = fm.MembersMemberId
JOIN Feature f ON f.FeatureId = fm.FeaturesFeatureId
JOIN Geography g ON g.MailAddressId = a.MailAddressId
WHERE
    f.FeatureId IN (1, 2)
ORDER BY f.FeatureId

My DbContext class is:

namespace Mapping.Data
{
    public class MappingContext : DbContext
    {
        public MappingContext(DbContextOptions<MappingContext> options)
            : base(options)
        {
        }

        public DbSet<Mapping.Models.Member> Member { get; set; } = default!;
        public DbSet<Mapping.Models.Feature> Feature { get; set; } = default!;
        public DbSet<Mapping.Models.MailAddress> MailAddress { get; set; } = default!;
        public DbSet<Mapping.Models.Geography> Geography { get; set; } = default!;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Member>()
                .HasOne(a => a.MailAddress)
                .WithOne(m => m.Member);

            modelBuilder.Entity<Member>()
                .HasMany(f => f.Features)
                .WithMany(m => m.Members);

            modelBuilder.Entity<MailAddress>()
                .HasOne(g => g.Geography)
                .WithOne(a => a.MailAddress);
        }
    }
}

How can I build this query using EF Core/Linq?


Solution

  • EF is designed to take over responsibility for generating SQL for Linq Queries against the object model automatically. While it does support explicit Linq QL with explicit joins, the use of this is the exception not the norm. Instead, leverage the navigation properties in the object model and let EF generate the suitable query:

    Looking at this SQL with what I can make from your entity object model:

    SELECT
        m.MemberMailingName,a.AddressStreetNumber,a.AddressStreetName,a.AddressCity,a.AddressState,a.AddressZip,g.Latitude,g.Longitude,f.FeatureName
    FROM
        Member m
    JOIN MailAddress a ON m.MemberId = a.MemberId
    JOIN FeatureMember fm ON m.MemberId = fm.MembersMemberId
    JOIN Feature f ON f.FeatureId = fm.FeaturesFeatureId
    JOIN Geography g ON g.MailAddressId = a.MailAddressId
    WHERE
        f.FeatureId IN (1, 2)
    ORDER BY f.FeatureId
    

    ... you should get the same results from something like this:

    int[] allowedFeatureIds = {1, 2};
    
    var data = await _context.Members
        .Where(m => m.Features.Any(f => allowedFeatureIds.Contains(f.Id)))
        .SelectMany(m => m.Features.Select(f => new 
        {
            m.MemberMailingName,
            m.MailAddress.AddressStreetNumber,
            m.MailAddress.AddressStreetName,
            m.MailAddress.AddressCity,
            m.MailAddress.AddressState,
            m.MailAddress.AddressZip,
            m.MailAddress.Geography.Latitude,
            m.MailAddress.Geography.Longitude,
            f.FeatureName
        })).ToListAsync();
    

    ... Assuming you want to just consume the data values in-code. If returning the data, constructing a DTO or view model class for those contents and populating that instead of the anonymous type.