Hub Contract Interface Inheritance
SignalRGen
supports composing your server-to-client (TServer
) and client-to-server (TClient
) contracts using C# interface inheritance. This lets you split large contracts into smaller, reusable pieces and assemble them per hub.
📖 TL;DR: How it works
SignalRGen
flattens the full interface hierarchy of TServer
and TClient
and treats all inherited methods as part of the hub contract.
Why use inheritance?
- Reuse common operations across hubs
- Keep contracts small and focused
- Add features without breaking existing hubs
- Version contracts incrementally
Quick examples
Server-to-client: compose events from multiple bases
csharp
public interface ICommonPresenceEvents
{
Task UserJoined(string user);
Task UserLeft(string user);
}
public interface IModerationEvents
{
Task UserMuted(string user);
Task UserUnmuted(string user);
}
public interface IChatHubServerToClient : ICommonPresenceEvents, IModerationEvents
{
Task MessageReceived(ChatMessage message);
}
Client-to-server: compose commands from multiple bases
csharp
public interface IMessageCommands
{
Task SendMessage(string message);
}
public interface IModerationCommands
{
Task MuteUser(string user);
Task UnmuteUser(string user);
}
public interface IChatHubClientToServer : IMessageCommands, IModerationCommands
{
Task<List<string>> GetActiveUsers();
}
Use them in your hub contract as usual:
csharp
using SignalRGen.Generator;
[HubClient(HubUri = "chat")]
public interface IChatHubContract : IBidirectionalHub<IChatHubServerToClient, IChatHubClientToServer>
{
// No methods here and no inheritance here
}
How inheritance is processed
- Flattening:
SignalRGen
collects methods from the interface and all its base interfaces (including multiple levels). - Multiple inheritance: You can inherit from any number of base interfaces.
- Directional separation:
TServer
andTClient
are processed independently; name collisions between them are fine.
Rules and constraints
- Method rules are unchanged:
- Server-to-client (
TServer
): methods must returnTask
- Client-to-server (
TClient
): methods must returnTask
orTask<TResult>
- Any number of parameters;
CancellationToken
is injected automatically, do not include it
- Server-to-client (
- Duplicates:
- Allowed if signatures are identical (same name, parameters, and return type)
- Conflicting signatures with the same name produce a broken client (see issue #67)
- Ignored members:
- Properties, events, indexers, static members, and default interface method bodies are ignored
- Accessibility:
- Base interfaces must be accessible to the project using
SignalRGen
- Base interfaces must be accessible to the project using
- Cycles:
- Circular inheritance is not allowed and results in a broken client
- Generics:
- Generic base interfaces are supported as long as they are closed where used
- Example:
ICrudCommands<OrderDto>
is fine; open genericICrudCommands<T>
must be closed in the derived interface
Examples
Multi-level inheritance
csharp
public interface IBasePresenceEvents
{
Task UserJoined(string user);
}
public interface IExtendedPresenceEvents : IBasePresenceEvents
{
Task UserLeft(string user);
}
public interface INewsFeedEvents
{
Task NewsPosted(string title, string content);
}
// SignalRGen will include all three events below
public interface IPortalServerToClient : IExtendedPresenceEvents, INewsFeedEvents
{
Task SystemMessage(string message);
}
Generic base interfaces
csharp
public interface ICrudCommands<TDto>
{
Task Create(TDto dto);
Task<TDto> GetById(string id);
Task Update(string id, TDto dto);
Task Delete(string id);
}
public record OrderDto(string Id, string Number);
public interface IOrdersClientToServer : ICrudCommands<OrderDto>
{
Task<List<OrderDto>> ListRecent(int count);
}
Versioning via inheritance
csharp
public interface IChatCommandsV1
{
Task SendMessage(string message);
}
public interface IChatCommandsV2 : IChatCommandsV1
{
Task EditMessage(string messageId, string newContent);
Task DeleteMessage(string messageId);
}
public interface IChatHubClientToServer : IChatCommandsV2
{
Task<List<string>> GetActiveUsers();
}