通用类型限制注入到类中

huangapple go评论90阅读模式
英文:

Limit type of generic injected into class

问题

我想知道在.NET6中是否可能实现类似这样的功能。

假设有一个应用程序,其中有3个以上的数据库,每个数据库都使用自己的DbContext,可以创建一个像这样的通用DbContext:

public class DbRepository<TRepository, TEntity> : IDbRepository<TRepository, TEntity> where TRepository : IContext

使用以下实现:

public class FirstDbContext : IFirstDbContext
public class SecondDbContext : ISecondDbContext

现在有一个与TRepository相同的BaseService:

public class BaseService<TRepository> where TRepository : IContext

我是否可以根据BaseService中的TRepository来限制传递给Service的内容?

所以:

public class UserService : BaseService<IFirstDbContext>
{
    public UserService(
        IDbRepository<IFirstDbContext, User> userDbRepository, /* 允许 */
        IDbRepository<ISecondDbContext, Invoice> invoiceDbRepository, /* 不允许 */
    ) { }
}

如果UserService使用BaseService<IFirstDbContext>定义,则只允许注入类型为IDbRepository<IFirstDbContext, TEntity>的内容。

英文:

I'm wondering if something like this is possible in .NET6.

Given an application with 3 more databases, each using its own DbContext, create a generic DbContext over an entity like so:

public class DbRepository&lt;TRepository, TEntity&gt; : IDbRepository&lt;TRepository, TEntity&gt; where TRepository : IContext

services.AddScoped(typeof(IDbRepository&lt;,&gt;), typeof(DbRepository&lt;,&gt;));

With implementations:

public class FirstDbContext : IFirstDbContext
public class SecondDbContext : ISecondDbContext

Now a BaseService that is also of the same TRepository:

public class BaseService&lt;TRepository&gt; where TRepository : IContext

Can I limit what is passed into a Service based on the TRepository in BaseService?

So That:

public class UserService : BaseService&lt;IFirstDbContext&gt;
{
    public UserService(
        IDbRepository&lt;IFirstDbContext, User&gt;  userDbRepository, /* allowed */
        IDbRepository&lt;ISecondDbContext, Invoice&gt; invoiceDbRepository, /* not allowed */
    ) { }
}

If UserService is defined with BaseService&lt;IFirstDbContext&gt; then only allow injection of types IDbRepository&lt;IFirstDbContext, TEntity&gt;.

答案1

得分: 2

不,至少不使用语言类型系统。您唯一能做的是编写自定义的 Roslyn 分析器,该分析器将处理您的文件并生成相应的警告/错误。

英文:

No, at least using language type system. The only thing you can do - write custom Roslyn analyzer which will process your files and emit corresponding warnings/errors.

答案2

得分: 2

我可以根据BaseService中的TRepository来限制传递给Service的内容吗?

可以,但有一些条件。


一种常见的技巧(至少对我来说如此)是向我的所有EF实体class类型添加一个标记接口,以便我可以将其添加为泛型类型参数约束,作为我编写的通用方法中的“合理性检查”,例如DbSet&lt;T&gt;,以确保T是实际的EF实体类类型,而不仅仅是.NET类型(毕竟,使用DbSet&lt;String&gt;会编译通过,尽管这只是愚蠢的) - 通常这看起来像这样 interface IEntity {}

(关于这个问题的一个相关示例,请参阅我在这个QA的答案中如何使用interface IPart1

我想强调的是,这个IEntity类型并不是绝对可靠的:下游的消费者完全可以自己实现IEntity并将其类型传递给受约束的通用方法,从而破坏事情 - 但这并不是我们目前关心的事情。

继续,首先,让我们摆脱每个DbContextinterface类型(即IFirstDbContextISecondDbContext):这只是不必要的:不要指定IFirstDbContext,直接指定FirstDbContext(尽管如果你真的想保留这些接口并使用它们,你可以这样做 - 但这只是为自己创造了额外的工作;你永远不会需要模拟DbContext,所以我只是不明白为什么要为DbContext类型创建一个interface(除了某种IReadOnlyDbContext之类的东西,但那是另一个话题)。


考虑到interface IEntity,所以让我们将其概括为一个通用接口:

interface IEntity&lt;TContext&gt;
    where TContext : DbContext
{
}

...然后将该接口添加到您的实体类型:UserInvoiceSome2ndDbContextEntity

partial class User : IEntity&lt;FirstDbContext&gt;
{
}

partial class Invoice : IEntity&lt;FirstDbContext&gt;
{
}

partial class Some2ndDbContextEntity: IEntity&lt;SecondDbContext&gt;
{
}

我非常反对泛型仓储的概念,所以我只是在这里重用了OP的IDbRepository&lt;TContext,TEntity&gt;接口,以阐明我所提出的观点 - 而不是作为C#程序应该编写的示例。

因此,让我们修改您的IDbRepository以添加一些约束:

interface IDbRepository&lt;TContext,TEntity&gt;
    where TContext : DbContext 
    where TEntity  : class, IEntity&lt;TContext&gt;
{
}

然后在UsersService内部,我们现在可以使用IDbRepository&lt;,&gt;

class GoodUserService
{
    public GoodUserService(
        IDbRepository&lt;FirstDbContext,User&gt;    usersRepository,
        IDbRepository&lt;FirstDbContext,Invoice&gt; invoicesRepository
    )
    {
    }
}

...但是如果我们尝试在Invoice中使用IDbRepositorySecondDbContext - 或者在Some2ndDbContextEntity中使用FirstDbContext,那么泛型约束将被违反:

class BadUserService
{
    public BadUserService(
        IDbRepository&lt;FirstDbContext,User                  &gt; usersRepository,
        IDbRepository&lt;FirstDbContext,Some2ndDbContextEntity&gt; badRepository
    )
    {
    }
}

确实,如果我们这样做,就会出现编译时错误:

通用类型限制注入到类中

英文:

> Can I limit what is passed into a Service based on the TRepository in BaseService?

Yes - ish.


A common technique (well, at least for me) is to add a marker interface to all of my EF entity class types so that I can add it as a generic-type-parameter-constraint as a sanity-check in generic methods that I write over, say, DbSet&lt;T&gt;, to ensure T is an actual EF entity class type rather than any-ol'-.NET-type (after-all, using DbSet&lt;String&gt; will compile, even though that's just silly) - usually this just looks like interface IEntity {}.

(For a somewhat relevant example, see how I use interface IPart in my answer to this other QA)

I'd like to stress that this IEntity type is not foolproof: it's entirely possible for downstream consumers to implement IEntity themselves and pass their types into the constrained generic methods and break things - but that's not something we're worried about at this point.

Moving on, first, let's ditch the interface types for each DbContext (i.e. IFirstDbContext and ISecondDbContext): that's just unnecessary: instead of specifying IFirstDbContext just specify FirstDbContext directly (though if you really want to keep those interfaces and use them, you can - but you're just creating extra legwork for yourself; you're never going to need to mock a DbContext so I just don't see the point in having an interface for a DbContext type (excepting some kind of IReadOnlyDbContext, but that's another topic).


Considering the interface IEntity So let's generalize that out to a single generic interface:

interface IEntity&lt;TContext&gt;
    where TContext : DbContext
{
}

...then add that interface to your entity types: User, Invoice, and Some2ndDbContextEntity:

partial class User : IEntity&lt;FirstDbContext&gt;
{
}

partial class Invoice : IEntity&lt;FirstDbContext&gt;
{
}

partial class Some2ndDbContextEntity: IEntity&lt;SecondDbContext&gt;
{
}

<sub>I'm very opposed to the concept of generic-repositories, so I'm only re-using the OP's IDbRepository&lt;TContext,TEntity&gt; interface here to illustrate the point I'm making - and not as an example of how C# programs should be written.</sub>

So let's modify your IDbRepository to add some constraints:

interface IDbRepository&lt;TContext,TEntity&gt;
    where TContext : DbContext 
    where TEntity  : class, IEntity&lt;TContext&gt;
{
}

So then inside UsersService we can now use IDbRepository&lt;,&gt;:

class GoodUserService
{
    public GoodUserService(
        IDbRepository&lt;FirstDbContext,User&gt;    usersRepository,
        IDbRepository&lt;FirstDbContext,Invoice&gt; invoicesRepository
    )
    {
    }
}

...but if we try to use IDbRepository for Invoice with SecondDbContext - or for Some2ndDbContextEntity with FirstDbContext then the generic constraints will be violated:

class BadUserService
{
    public BadUserService(
        IDbRepository&lt;FirstDbContext,User                  &gt; usersRepository,
        IDbRepository&lt;FirstDbContext,Some2ndDbContextEntity&gt; badRepository
    )
    {
    }
}

Indeed, we see a compile-time error if we do that:

通用类型限制注入到类中

huangapple
  • 本文由 发表于 2023年7月12日 21:48:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/76671302.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定