C#:扩展泛型类

10
partial class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
}

我的通用存储库实现了对 TEntity 的一组常见方法,例如:

public TEntity Get(int id)
{
    return _context.Set<TEntity>()
        .Find(id);
}

public TEntity Get(Expression<Func<TEntity, bool>> predicate)
{
    return _context.Set<TEntity>()
}

我可以像这样访问:

Repository<User>().Get();

许多存储库执行相同的操作,因此这是有益的,但现在我想扩展 Repository<User> 以支持一些额外的行为。

partial class Repository<User> : IRepository<User> 
{
    public user DoMagicFunction()
    {
    }
}

这样我就可以像使用仓库一样使用它了

Repository<User>().DoMagicFunction();

如何扩展相同的泛型类以便为Tentity扩展新行为而不是修改它。

我可以像创建另一个UserRepository来支持新的特性一样做,但访问器会变成

UserRepository.DoMagicFunction();

但我希望它能够像这样


Repository<User>().DoMagicFunction();
5个回答

11

你可以使用一个扩展方法

public static class ExtensionMethods {

    public static User DoMagicFunction(<b>this</b> Repository<User> repository) {
        // some magic
        return null; //or another user
    } 

}

这样做将以语法上良好的方式将该函数添加到Repository<User>对象中。

如果您想要支持不仅适用于User,而且还适用于User子类,您可以使该函数通用:

public static class ExtensionMethods {

    public static <b>TEntity</b> DoMagicFunction<b><TEntity></b>(<b>this</b> Repository<TEntity> repository)
        <b>where TEntity : User</b> {
        // some magic
        return null; //or another <b>TEntity</b>
    } 

}

真的,但也许将TUser更改为TEntity会更好。 - Janne Matikainen
这个扩展方法如何访问Repository<T>的私有DataContext成员,以便选择它需要执行工作的数据? - Peter Morris
@WillemVanOnsem 这违反了开闭原则。 - Peter Morris
@PeterMorris:不是这样的。软件设计当然应该考虑原则。但经常被忽视的原则之一是生成“最小更改”。internal提供了额外的保护级别,防止将事物变为public - Willem Van Onsem
让我们在聊天中继续这个讨论 - Peter Morris
显示剩余6条评论

3
C#有一种语言特性叫做扩展方法,你可能在使用.NET框架时不知道地使用它们(例如linq扩展方法)。通常情况下,我们可以通过扩展方法来扩展你的类或者接口,而不会破坏原本代码的功能。以下是一个例子。
假设你有一个泛型的IRepository接口:
public interface IRepository<TEntity> where TEntity : class, IEntity
{
    IQueryable<TEntity> Entities { get; }
}

这个接口遵循SOLID原则,尤其是OI原则。
现在假设IEntity长成这样:
public interface IEntity
{
    int Id { get; }
}

现在,您可以完美地想象一个经常可重复使用的扩展方法,就像这样:
public static class RepositoryExtensions
{
    // similar to your MagicFunction
    public static TEntity GetById<TEntity>(this IRepository<TEntity> repository, int id)
         where TEntity : class, IEntity
    {
        return repository.Entities.Single(entity => entity.Id == id);
    }
}

以类似的方式,您还可以扩展您的Repository类。
public static class RepositoryExtensions
{
    public static TEntity GenericMagicFunction<TEntity>(this Repository<TEntity> repository)
    {
         //do some stuff
    }
}

现在您可以像这样使用它:
var repository = new Repository<User>();
var user = repository.GenericMagicFunction();

你也可以限制你的扩展方法:

public static class RepositoryExtensions
{
    public static User DoMagicFunction(this Repository<User> repository)
    {
         //do some stuff
    }
}

但这样做会失去它的目的,您可以在Repository<User>类中实现它。如果您的系统和架构使用依赖注入,那么您很可能会向消费类注入IRepository<User>。因此,我提供的第一个或第二个扩展方法示例最有意义。

扩展方法如何访问Repository<T>中的私有DataContext成员,以便使用此方法检索数据? - Peter Morris
为什么你想要访问一个private field?!它是私有的,有其原因;)如果你需要这样的东西,那就意味着设计有问题,你需要重新考虑你的设计。我甚至不会在像Repository这样的具体服务上创建扩展方法,但这完全取决于你的架构。我倾向于使用SOLID设计的架构。但你可能使用的是糟糕或遗留的架构。Microsoft也在像IEnumerable<T>这样的接口上有linq扩展方法。 - QuantumHive
你不想访问私有字段,但你想从子类访问受保护的字段。我很高兴你认为将字段暴露以允许外部访问实用方法是一种不好的做法,并且这样做暗示需要重新设计。这就是为什么我建议不要将该字段设置为 Internal 并在我的答案中重新设计存储库的原因 :) - Peter Morris
扩展方法的有用性正好在于您正在“扩展”的类或接口的public成员上。我在我的答案中也提到,如果您在具体的Repository类上创建扩展方法,则会破坏其目的。我认为我提供的GetById示例是如何扩展像IRepository<T>这样的通用组件的完美示例。让我们不要在评论中继续讨论,而是在聊天室中进行。 - QuantumHive

1
如果您想扩展任何存储库,可以按照以下步骤进行。
public static class RepositoryExtension
{
    public static void MagicMethod<TEntity>(this IRepository<TEntity> repo) where TEntity: class
    {
        ....
    }
}

针对特定的代码库(例如用户代码库),您可以使用类似的过程。
public static class RepositoryExtension
{
    public static void MagicMethod(this IRepository<User> repo) 
    {
        ....
    }
}

你写的这个东西 - 为什么要写扩展方法而不是部分类呢?我认为 OP 的问题是只为特定类型的 TEntity(即 User)创建一个方法。 - decPL

1

扩展方法不是最好的选择,因为实现该方法的代码只能访问扩展的类的公共/内部成员,并且您可能希望将存储库的 DataContext 设置为私有。

我认为您的方法需要略作改变。

如果以后您想要向通用存储库添加删除方法,但您有一些永远不应该被删除的实体,那么您将得到一个像 PurchaseOrder 这样的存储库实例,您要么必须记住永远不要调用 delete 方法,要么就必须创建一个 Repository<T> 的子类,如果调用则抛出 InvalidOperationException。这两种方法都不是很好的实现。

相反,您应该完全删除您的 IRepository<T> 接口。保留您的 Repository<T> 类,但明确定义每个实体的存储库接口,仅具有您需要的方法。

public class Repository<TKey, TEntity>......
{
  public TEntity Get<TEntity>(TKey key)....
  public void Delete(TEntity instance)....
  ...etc...
}

public interface IPurchaseOrderRepository {
  PurchaseOrder Get(int orderNumber);
  // Note: No delete is exposed
}

MyDependencyInjection.Register<IPurchaseOrderRepository, Repository<PurchaseOrder, int>>();

当您需要在存储库上添加其他方法时,您可以将它们添加到您的IPurchaseOrderRepository并创建Repository<T>的子类。

public interface IPurchaseOrderRepository {
  PurchaseOrder Get(int orderNumber);
  void DoSomethingElse(int orderNumber);
}

public class PurchaseOrderRepository: Repository<PurchaseOrder, int> {
  public void DoSomethingElse(int orderNumber) {.......}
}


MyDependencyInjection.Register<IPurchaseOrderRepository, PurchaseOrderRepository>();

1
OP只是想扩展他的接口,这可以通过一种语言特性来实现(在我看来,这只是一个技术问题)。你的答案涉及到了更广泛的范围,可能会引发关于如何使用和应用设计模式、分层架构以及一些软件原则,比如SOLID原则的讨论。我认为告诉OP他的设计应该不同不是我们的工作,应该开启另一个主题去讨论这个问题。你的答案很主观,我不认为它是一个好答案。 - QuantumHive
1
虽然有点跑题,但还是感谢这个答案。 - Shekhar Pankaj
@QuantumHive 我的回答客观地解释了如何实现他想要做的事情,即使用基础的 Repository<T> 类,并在某些仓库接口上添加其他方法。我还进一步解释了为什么他应该采用这种方法而不是其他建议。你对我的回答的看法是主观的,我不认为这是一个好的看法;-) - Peter Morris
@ShekharPankaj 它提供了比你需要解决问题所需更多的信息,但它确实解决了你的问题,因此它不是离题。我只是觉得权衡一下你的其他选择,然后解释为什么这个是最好的会很有帮助 :) - Peter Morris
@PeterMorris 再次提醒,你的回答是主观的,因为你提供了 OP 设计的另一种方法。我认为你的设计很糟糕,我可以确定至少有一个人会同意,那就是 Martin Fowler。你使用 Repository Pattern 的想法是错误的。正如它所述:*仓储模式在领域和数据映射层之间进行中介...*。你的设计将业务逻辑和领域逻辑混合在仓储模式中。此外,好的设计范围对于这个问题来说太大了,应该在另一个线程中开始讨论。 - QuantumHive
显示剩余2条评论

1

扩展方法是这种情况的最佳选择。

注意:我没有检查,但你应该检查依赖注入是否仍然像往常一样正常工作。

您可以使用以下代码进行测试:

public class Employee
{
}

public class User
{
}

public interface IRepo<TEntity> where TEntity : class
{
    TEntity Get(int id);
    DbSet<TEntity> Get(Expression<Func<TEntity, bool>> predicate);
    DbContext GetContext();
}

public class Repo<TEntity> : IRepo<TEntity> where TEntity : class
{
    DbContext _context;
    public TEntity Get(int id)
    {
        return _context.Set<TEntity>()
                       .Find(id);
    }

    public DbSet<TEntity> Get(Expression<Func<TEntity, bool>> predicate)
    {
        return _context.Set<TEntity>();
    }

    public DbContext GetContext()
    {
        return _context;
    }
}

public static class RepoExtensions
{
    public static ChangeTracker DoMagic(this Repo<User> userRepo)
    {
        return userRepo.GetContext().ChangeTracker;
    }
}

public static class Test
{
    public static void DoTest()
    {
        Repo<User> repoUser = new Repo<User>();
        repoUser.DoMagic();

        Repo<Employee> repoEmployee = new Repo<Employee>();
        //repoEmployee.DoMagic();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接