Why do Entity Framework Core POCOs returned from a DbQuery have to be public?

.net .net-core .net-standard access-modifiers entity-framework-core

Question

I'm using a Microsoft.EntityFrameworkCore.DbQuery to return POCOs from the result of a table-valued function in my database. I noticed that none of my POCOs have any of their properties set -- they're all default values -- as long as their access modifiers are anything other than public. In my case, the POCOs are all implementing interfaces explicitly so the consumer doesn't (or shouldn't) know anything about the POCO definitions so I want to set them to internal.

Why do the POCOs have to be public? Or, is there a way to make it so they don't have to be public? Here is some sample code to demonstrate this:

CoreLibrary.dll (.NET Standard 2.0)

using System.Collections.Generic;

namespace CoreLibrary
{
    public interface IWidgetRepository
    {
        IReadOnlyCollection<IWidget> FindAllWidgets();
        IReadOnlyCollection<IWidget> FindWidgets(string withText);
    }

    public interface IWidget
    {
        int ID { get; }
        string Name { get; }
        string Value { get; }
    }
}

DataAccess.dll (.NET Standard 2.0)

using CoreLibrary;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;

namespace DataAccess
{
    public sealed class WidgetRepositoryApi : IWidgetRepository
    {
        private DatabaseContext _dbContext;

        public WidgetRepositoryApi(string databaseServerName, string databaseName)
            => _dbContext = new DatabaseContext(databaseServerName, databaseName);

        public IReadOnlyCollection<IWidget> FindAllWidgets()
            => _dbContext.Widget.ToList();

        public IReadOnlyCollection<IWidget> FindWidgets(string withText)
        {
            string sqlQuery = "SELECT * FROM dbo.fn_SearchWidgetText(@Text)";

            var ret = _dbContext.GetWidgetFullTextSearchMatches
                .FromSql(sqlQuery, new SqlParameter("@Text", 
                System.Data.SqlDbType.NVarChar) { Value = withText });

            return ret.ToList().AsReadOnly();
        }
    }

    internal partial class DatabaseContext : DbContext
    {
        private string _dbConnectionString;

        internal DatabaseContext(string databaseServerName, string databaseName) 
            => _dbConnectionString = 
            $"Server={databaseServerName};Database={databaseName};Trusted_Connection=True;";

        internal virtual DbSet<DBSetWidget> Widget { get; set; }

        internal virtual DbQuery<DBQueryWidget> GetWidgetFullTextSearchMatches { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer(_dbConnectionString);
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<DBSetWidget>(entity =>
            {
                entity.Property(e => e.Id).HasColumnName("ID");

                entity.Property(e => e.Name)
                    .IsRequired()
                    .HasMaxLength(50);

                entity.Property(e => e.Value).IsRequired();
            });
        }
    }

    /********************************************************************
     * NOTE: there need to be two widget implementations (POCOs)
     * because otherwise the code throws
     * System.InvalidCastException: 'Unable to cast object of type 
     * 'Microsoft.EntityFrameworkCore.Internal.
     * InternalDbQuery`1[DataAccess.DBSetWidget]' to type 
     * 'Microsoft.EntityFrameworkCore.DbSet`1[DataAccess.DBSetWidget]'.'
     *******************************************************************/

    internal partial class DBQueryWidget : IWidget
    {
        internal int Id { get; set; }
        internal string Name { get; set; }
        internal string Value { get; set; }
        int IWidget.ID => Id;
        string IWidget.Name => Name;
        string IWidget.Value => Value;
    }

    internal partial class DBSetWidget : IWidget
    {
        internal int Id { get; set; }
        internal string Name { get; set; }
        internal string Value { get; set; }
        int IWidget.ID => Id;
        string IWidget.Name => Name;
        string IWidget.Value => Value;
    }
}

ConsumerConsoleApp.exe (.NET Core 2.1)

using DataAccess;

namespace ConsumerConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var repo = new WidgetRepositoryApi(args[0], args[1]);
            var allWidgets = repo.FindAllWidgets();
            var someWidgets = repo.FindWidgets("facebook");
        }
    }
}

Widget Table in SQL Database

CREATE TABLE [dbo].[Widget](
[ID] [INT] IDENTITY(1,1) NOT NULL,
[Name] [NVARCHAR](50) NOT NULL,
[Value] [NVARCHAR](MAX) NOT NULL,
CONSTRAINT [PK_Widget] PRIMARY KEY CLUSTERED 
(
[ID] ASC
)WITH (PAD_INDEX = OFF, 
STATISTICS_NORECOMPUTE = OFF, 
IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS = ON, 
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

fn_SearchWidgetText in database

CREATE FUNCTION [dbo].[fn_SearchWidgetText]
(   
    @Text NVARCHAR(1000)
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT * 
    FROM dbo.Widget
    WHERE CONTAINS(Value, @Text)
)

enter image description here

enter image description here

If I change all my access modifiers in my DataAccess namespace to public, now the POCOs get filled properly:

enter image description here

1
1
10/31/2018 7:24:13 PM

Popular Answer

The short answer is just because it's by design.

The observation is a state of any object can be split into two parts. The first part is public state, and the second part is internal state of the object. When you design data store technology you should decide what you want to store. Only one part or both? EF is designed to store almost any object (which can have complicated internal structure) in relatively simple database. So, is a good assumption to ignore internal structure of objects.

1
10/30/2018 7:59:01 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow