Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/MagicOnion.Abstractions/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ MagicOnion.GenerateDefineDebugAttribute
MagicOnion.GenerateDefineDebugAttribute.GenerateDefineDebugAttribute() -> void
MagicOnion.GenerateIfDirectiveAttribute
MagicOnion.GenerateIfDirectiveAttribute.GenerateIfDirectiveAttribute(string! condition) -> void
MagicOnion.ServiceNameAttribute
MagicOnion.ServiceNameAttribute.Name.get -> string!
MagicOnion.ServiceNameAttribute.ServiceNameAttribute(string! name) -> void
41 changes: 41 additions & 0 deletions src/MagicOnion.Abstractions/ServiceNameAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace MagicOnion;

/// <summary>
/// Specifies a custom gRPC service name for the MagicOnion service or StreamingHub interface.
/// When applied, this name is used instead of the default interface type name for gRPC routing.
/// This allows multiple service interfaces with the same short name but different namespaces
/// to coexist on the same server.
/// </summary>
/// <remarks>
/// The attribute must be applied consistently on the shared interface that is referenced
/// by both client and server. The specified name becomes part of the gRPC method path
/// (e.g., <c>/Custom.ServiceName/MethodName</c>).
/// </remarks>
/// <example>
/// <code>
/// [ServiceName("MyNamespace.IMyService")]
/// public interface IMyService : IService&lt;IMyService&gt;
/// {
/// UnaryResult&lt;string&gt; HelloAsync();
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = true)]
public sealed class ServiceNameAttribute : Attribute
{
/// <summary>
/// Gets the custom service name used for gRPC routing.
/// </summary>
public string Name { get; }

/// <summary>
/// Initializes a new instance of <see cref="ServiceNameAttribute"/> with the specified service name.
/// </summary>
/// <param name="name">The custom service name to use for gRPC routing.</param>
public ServiceNameAttribute(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Service name cannot be null or whitespace.", nameof(name));
Name = name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace MagicOnion.Client.SourceGenerator.CodeAnalysis;
public interface IMagicOnionServiceInfo
{
MagicOnionTypeInfo ServiceType { get; }
string ServiceName { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ namespace MagicOnion.Client.SourceGenerator.CodeAnalysis;
public class MagicOnionServiceInfo : IMagicOnionServiceInfo
{
public MagicOnionTypeInfo ServiceType { get; }
public string ServiceName { get; }
public IReadOnlyList<MagicOnionServiceMethodInfo> Methods { get; }

public MagicOnionServiceInfo(MagicOnionTypeInfo serviceType, IReadOnlyList<MagicOnionServiceMethodInfo> methods)
public MagicOnionServiceInfo(MagicOnionTypeInfo serviceType, string serviceName, IReadOnlyList<MagicOnionServiceMethodInfo> methods)
{
ServiceType = serviceType;
ServiceName = serviceName;
Methods = methods;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ namespace MagicOnion.Client.SourceGenerator.CodeAnalysis;
public class MagicOnionStreamingHubInfo : IMagicOnionServiceInfo
{
public MagicOnionTypeInfo ServiceType { get; }
public string ServiceName { get; }
public IReadOnlyList<MagicOnionHubMethodInfo> Methods { get; }
public MagicOnionStreamingHubReceiverInfo Receiver { get; }

public MagicOnionStreamingHubInfo(MagicOnionTypeInfo serviceType, IReadOnlyList<MagicOnionHubMethodInfo> methods, MagicOnionStreamingHubReceiverInfo receiver)
public MagicOnionStreamingHubInfo(MagicOnionTypeInfo serviceType, string serviceName, IReadOnlyList<MagicOnionHubMethodInfo> methods, MagicOnionStreamingHubReceiverInfo receiver)
{
ServiceType = serviceType;
ServiceName = serviceName;
Methods = methods;
Receiver = receiver;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ static IReadOnlyList<MagicOnionStreamingHubInfo> GetStreamingHubs(MethodCollecto

var receiver = new MagicOnionStreamingHubInfo.MagicOnionStreamingHubReceiverInfo(receiverType, receiverMethods);

return new MagicOnionStreamingHubInfo(serviceType, methods, receiver);
return new MagicOnionStreamingHubInfo(serviceType, GetServiceNameFromSymbol(x, serviceType), methods, receiver);
})
.Where(x => x is not null)
.Cast<MagicOnionStreamingHubInfo>()
Expand All @@ -111,6 +111,16 @@ static int GetHubMethodIdFromMethodSymbol(IMethodSymbol methodSymbol)
static bool HasIgnoreAttribute(ISymbol symbol)
=> symbol.GetAttributes().FindAttributeShortName("IgnoreAttribute") is not null;

static string GetServiceNameFromSymbol(INamedTypeSymbol interfaceSymbol, MagicOnionTypeInfo serviceType)
{
var serviceNameAttr = interfaceSymbol.GetAttributes().FindAttributeShortName("ServiceNameAttribute");
if (serviceNameAttr is not null && serviceNameAttr.ConstructorArguments.Length > 0 && serviceNameAttr.ConstructorArguments[0].Value is string name)
{
return name;
}
return serviceType.Name;
}

static bool TryCreateHubMethodInfoFromMethodSymbol(MethodCollectorContext ctx, MagicOnionTypeInfo interfaceType, IMethodSymbol methodSymbol, [NotNullWhen(true)] out MagicOnionStreamingHubInfo.MagicOnionHubMethodInfo? methodInfo, out Diagnostic? diagnostic)
{
var hubId = GetHubMethodIdFromMethodSymbol(methodSymbol);
Expand Down Expand Up @@ -200,12 +210,13 @@ static IReadOnlyList<MagicOnionServiceInfo> GetServices(MethodCollectorContext c
return null;
}

var serviceName = GetServiceNameFromSymbol(x, serviceType);
var methods = new List<MagicOnionServiceInfo.MagicOnionServiceMethodInfo>();
var hasError = false;
foreach (var methodSymbol in x.GetMembers().OfType<IMethodSymbol>())
{
if (HasIgnoreAttribute(methodSymbol)) continue;
if (TryCreateServiceMethodInfoFromMethodSymbol(ctx, serviceType, methodSymbol, out var methodInfo, out var diagnostic))
if (TryCreateServiceMethodInfoFromMethodSymbol(ctx, serviceType, serviceName, methodSymbol, out var methodInfo, out var diagnostic))
{
methods.Add(methodInfo);
}
Expand All @@ -225,15 +236,15 @@ static IReadOnlyList<MagicOnionServiceInfo> GetServices(MethodCollectorContext c
return null;
}

return new MagicOnionServiceInfo(serviceType, methods);
return new MagicOnionServiceInfo(serviceType, serviceName, methods);
})
.Where(x => x is not null)
.Cast<MagicOnionServiceInfo>()
.OrderBy(x => x.ServiceType.FullName)
.ToArray();
}

static bool TryCreateServiceMethodInfoFromMethodSymbol(MethodCollectorContext ctx, MagicOnionTypeInfo serviceType, IMethodSymbol methodSymbol, [NotNullWhen(true)] out MagicOnionServiceInfo.MagicOnionServiceMethodInfo? serviceMethodInfo, out Diagnostic? diagnostic)
static bool TryCreateServiceMethodInfoFromMethodSymbol(MethodCollectorContext ctx, MagicOnionTypeInfo serviceType, string serviceName, IMethodSymbol methodSymbol, [NotNullWhen(true)] out MagicOnionServiceInfo.MagicOnionServiceMethodInfo? serviceMethodInfo, out Diagnostic? diagnostic)
{
var methodReturnType = ctx.GetOrCreateTypeInfoFromSymbol(methodSymbol.ReturnType);
var methodParameters = CreateParameterInfoListFromMethodSymbol(ctx, methodSymbol);
Expand Down Expand Up @@ -306,9 +317,9 @@ static bool TryCreateServiceMethodInfoFromMethodSymbol(MethodCollectorContext ct
diagnostic = null;
serviceMethodInfo = new MagicOnionServiceInfo.MagicOnionServiceMethodInfo(
methodType,
serviceType.Name,
serviceName,
methodSymbol.Name,
$"{serviceType.Name}/{methodSymbol.Name}",
$"{serviceName}/{methodSymbol.Name}",
methodParameters,
methodReturnType,
requestType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ static void EmitConstructor(StreamingHubClientBuildContext ctx)
{
ctx.Writer.AppendLineWithFormat($$"""
public {{ctx.Hub.GetClientFullName()}}({{ctx.Hub.Receiver.ReceiverType.FullName}} receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options, global::MagicOnion.Client.IStreamingHubDiagnosticHandler diagnosticHandler)
: base("{{ctx.Hub.ServiceType.Name}}", receiver, callInvoker, options)
: base("{{ctx.Hub.ServiceName}}", receiver, callInvoker, options)
{
this.diagnosticHandler = diagnosticHandler;
}
Expand All @@ -197,7 +197,7 @@ static void EmitConstructor(StreamingHubClientBuildContext ctx)
{
ctx.Writer.AppendLineWithFormat($$"""
public {{ctx.Hub.GetClientFullName()}}({{ctx.Hub.Receiver.ReceiverType.FullName}} receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options)
: base("{{ctx.Hub.ServiceType.Name}}", receiver, callInvoker, options)
: base("{{ctx.Hub.ServiceName}}", receiver, callInvoker, options)
{
}
""");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ static FieldInfo DefineConstructor(TypeBuilder typeBuilder, Type interfaceType,

// base("InterfaceName", receiver, callInvoker, options);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldstr, interfaceType.Name);
il.Emit(OpCodes.Ldstr, MagicOnion.Internal.ServiceNameHelper.GetServiceName(interfaceType));
il.Emit(OpCodes.Ldarg_1); // receiver
il.Emit(OpCodes.Ldarg_2); // callInvoker
il.Emit(OpCodes.Ldarg_3); // options
Expand Down Expand Up @@ -761,7 +761,7 @@ static MethodInfoCache()

class MethodDefinition
{
public string Path => ServiceType.Name + "/" + MethodInfo.Name;
public string Path => MagicOnion.Internal.ServiceNameHelper.GetServiceName(ServiceType) + "/" + MethodInfo.Name;

public Type ServiceType { get; set; }
public MethodInfo MethodInfo { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Grpc.Core;
using MagicOnion.Internal;
using MessagePack;

namespace MagicOnion.Client.DynamicClient;
Expand Down Expand Up @@ -52,12 +53,13 @@ public MagicOnionServiceMethodInfo(MethodType methodType, string serviceName, st
public static MagicOnionServiceMethodInfo Create(Type serviceType, MethodInfo methodInfo)
{
var (methodType, requestType, responseType) = GetMethodTypeAndResponseTypeFromMethod(methodInfo);
var resolvedServiceName = ServiceNameHelper.GetServiceName(serviceType);

var method = new MagicOnionServiceMethodInfo(
methodType,
serviceType.Name,
resolvedServiceName,
methodInfo.Name,
$"{serviceType.Name}/{methodInfo.Name}",
$"{resolvedServiceName}/{methodInfo.Name}",
methodInfo.GetParameters().Select(y => y.ParameterType).ToArray(),
methodInfo.ReturnType,
requestType ?? GetRequestTypeFromMethod(methodInfo),
Expand Down
24 changes: 24 additions & 0 deletions src/MagicOnion.Internal/ServiceNameHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Reflection;

namespace MagicOnion.Internal;

internal static class ServiceNameHelper
{
/// <summary>
/// Resolves the gRPC service name for a given service interface type.
/// If the interface has a <see cref="ServiceNameAttribute"/>, its value is used.
/// Otherwise, the short type name (<see cref="Type.Name"/>) is used as the default.
/// </summary>
/// <param name="serviceInterfaceType">The service interface type (e.g., IMyService).</param>
/// <returns>The resolved service name string.</returns>
public static string GetServiceName(Type serviceInterfaceType)
{
var attr = serviceInterfaceType.GetCustomAttribute<ServiceNameAttribute>();
if (attr is not null)
{
return attr.Name;
}

return serviceInterfaceType.Name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ public IReadOnlyList<IMagicOnionGrpcMethod> GetGrpcMethods<TService>() where TSe
.First(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IService<>))
.GenericTypeArguments[0];

var serviceName = ServiceNameHelper.GetServiceName(typeServiceInterface);

// StreamingHub
if (typeof(TService).IsAssignableTo(typeof(IStreamingHubBase)))
{
return [new MagicOnionStreamingHubConnectMethod<TService>(typeServiceInterface.Name)];
return [new MagicOnionStreamingHubConnectMethod<TService>(serviceName)];
}

// Unary, ClientStreaming, ServerStreaming, DuplexStreaming
Expand Down Expand Up @@ -144,7 +146,7 @@ public IReadOnlyList<IMagicOnionGrpcMethod> GetGrpcMethods<TService>() where TSe

try
{
var serviceMethod = Activator.CreateInstance(typeMethod.MakeGenericType(typeMethodTypeArgs), [typeServiceInterface.Name, targetMethod.Name, invoker])!;
var serviceMethod = Activator.CreateInstance(typeMethod.MakeGenericType(typeMethodTypeArgs), [serviceName, targetMethod.Name, invoker])!;
methods.Add((IMagicOnionGrpcMethod)serviceMethod);
}
catch (TargetInvocationException e)
Expand All @@ -169,6 +171,8 @@ public IReadOnlyList<IMagicOnionStreamingHubMethod> GetStreamingHubMethods<TServ
.First(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IService<>))
.GenericTypeArguments[0];

var serviceName = ServiceNameHelper.GetServiceName(typeServiceInterface);

var interfaceMap = typeServiceImplementation.GetInterfaceMapWithParents(typeServiceInterface);
for (var i = 0; i < interfaceMap.TargetMethods.Length; i++)
{
Expand Down Expand Up @@ -216,7 +220,7 @@ public IReadOnlyList<IMagicOnionStreamingHubMethod> GetStreamingHubMethods<TServ

try
{
var hubMethod = (IMagicOnionStreamingHubMethod)Activator.CreateInstance(hubMethodType, [typeServiceInterface.Name, methodInfo.Name, invoker])!;
var hubMethod = (IMagicOnionStreamingHubMethod)Activator.CreateInstance(hubMethodType, [serviceName, methodInfo.Name, invoker])!;
methods.Add(hubMethod);
}
catch (TargetInvocationException e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal class StreamingHubHandler : IEquatable<StreamingHubHandler>
readonly string toStringCache;
readonly int getHashCodeCache;

public string HubName => hubMethod.Metadata.StreamingHubInterfaceType.Name;
public string HubName => hubMethod.ServiceName;
public Type HubType => hubMethod.Metadata.StreamingHubImplementationType;
public MethodInfo MethodInfo => hubMethod.Metadata.ImplementationMethod;
public int MethodId => hubMethod.Metadata.MethodId;
Expand Down
Loading
Loading