【问题标题】:Why do Entity Framework Core POCOs returned from a DbQuery have to be public?为什么从 DbQuery 返回的 Entity Framework Core POCO 必须是公开的?
【发布时间】:2019-04-03 22:28:26
【问题描述】:

我正在使用Microsoft.EntityFrameworkCore.DbQuery 从我的数据库中的表值函数的结果中返回 POCO。我注意到我的所有 POCO 都没有设置任何属性——它们都是默认值——只要它们的访问修饰符不是public。就我而言,POCO 都明确地实现了接口,因此消费者不(或不应该)知道任何关于 POCO 定义的信息,因此我想将它们设置为 internal

为什么 POCO 必须公开?或者,有没有办法让它不必公开?下面是一些示例代码来证明这一点:

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");
        }
    }
}

SQL 数据库中的小部件表

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

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

如果我将我的 DataAccess 命名空间中的所有访问修饰符更改为 public,现在 POCO 将被正确填充:

【问题讨论】:

  • 他们真的不必公开。请显示未设置实体对象属性的示例。我无法想象 EF 如何在不设置属性值的情况下实现实体对象。
  • 我怎么能证明这一点?你需要数据库来测试它。
  • 如果只有一个类 + 接口 + 映射组合的行为方式与您描述的方式相同,那将会很有趣。为了测试,我们可以让 EF 生成一个数据库。
  • @GertArnold -- 我已经玩过它并缩小了一些范围。请查看更新。
  • 您将过多的 OOP 放入本应是简单存储模型的东西中。如果您想使用 EF 通用处理此类实体(例如具有接口约束的通用方法),则必须隐式(公开)实现接口。由于您的课程是internal,我不明白为什么它们的属性不应该是publicinternalinternal 类的属性的修饰符不会使它们更具内部性。

标签: .net .net-core entity-framework-core .net-standard access-modifiers


【解决方案1】:

简短的回答只是因为它是设计使然。

观察是任何对象的状态,可以分为两部分。第一部分是公共状态,第二部分是对象的内部状态。当您设计数据存储技术时,您应该决定要存储什么。只有一部分还是两者都有? EF 旨在将几乎任何对象(可能具有复杂的内部结构)存储在相对简单的数据库中。因此,忽略对象的内部结构是一个很好的假设。

【讨论】:

  • 我明白你的意思,但是覆盖默认行为仍然会很好,也许我们可以使用属性来指示 EF 应该关心的成员,而不是按照惯例进行配置。
  • 请查看我更新的问题。自从我发布 OP 以来,我已经了解了很多关于这个问题的复杂性。如果它是“按设计”,那么它就不一致,因为DbSet 返回的 POCO 工作得很好,只是在 DbQuery 返回时就不行。
【解决方案2】:

Entity Framework 核心不按其默认约定映射内部属性。 DBQueryWidgetDBSetWidget 之间的唯一区别是,对于后者,您有一个明确的映射指令:

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();
});

对于DBQueryWidget,没有这样的映射,因此 EF 遵循其默认约定并且不映射其内部属性。所以你唯一需要解决的就是为DBQueryWidget添加一个类似的映射。

【讨论】:

    猜你喜欢
    • 2023-03-07
    • 1970-01-01
    • 2021-09-10
    • 1970-01-01
    • 1970-01-01
    • 2015-02-04
    • 1970-01-01
    • 2016-12-21
    • 2021-06-18
    相关资源
    最近更新 更多