ASPNET Core SignalR 을 이용한 Push Server 구현 - 3 (vertical slice architecture)

2023. 7. 10. 00:00카테고리 없음

반응형

https://yogingang.tistory.com/437

 

ASPNET Core SignalR 을 이용한 Push Server 구현 - 1

signalR core 는 dotnet core 에서 실시간 통신을 위해 내놓은 비동기 양방향 통신 frameworks 이다. aspnet core 위에서 돌아가는 형태 이다. https://yogingang.tistory.com/404 Rest API Template 만들기 - EP 01 (Create , Swagger

yogingang.tistory.com

https://yogingang.tistory.com/439

 

ASPNET Core SignalR 을 이용한 Push Server 구현 - 2 (인증처리)

SignalR 관련해서도 인증처리가 가능 하다. 일반적으로 인증서버를 만들고 해당 서버로 부터 인증처리 하도록 한다. https://yogingang.tistory.com/412 Rest API Template 만들기 - EP 08 (DB 를 통해 AccessToken 및 Ref

yogingang.tistory.com

 

이제 rest api 를 추가하여 api 를 통해 특정 client 들에 push 하도록 해보겠다. 

client 들은 channel 에 접속만 되어 있고  관리자가 특정 channel  client 들에 실시간으로

message 를 전달하고 싶을 수도 있다.

이럴 경우 RestAPI 를  노출하여 channelId 를 입력으로 받고 client 들에 전송가능 하다.

 

아래와 같이 minimal api 를 추가해 보자

Program.cs 

var pushRouter = app.MapGroup("push").WithOpenApi();

pushRouter.MapPost("all", 
    async ([FromBody] ChannelMessageInfo messageInfo, IHubContext<ChannelHub> hubContext) 
    => await hubContext.Clients.Group(messageInfo.Id).SendAsync("receiveMessage", messageInfo.Message));

pushRouter.MapPost("user",
    async ([FromBody] ChannelMessageInfo messageInfo, IHubContext <ChannelHub> hubContext)
    => await hubContext.Clients.Group(messageInfo.Id).SendAsync("receiveMessage", messageInfo.Message));

ChannelMessageInfo

class ChannelMessageInfo
{
    public string Message { get; set; }
    public string Id { get; set; }   
}

 

signalR server 를 실행 시키자 .swagger 에 /push/all ,  /push/user 가 추가된 것을 확인 가능하다

이 후 client 를 실행 시키고 channel id 와 join id 를 알아내자

 

그리고 all 와 user 를 실행 시키면 restapi 를 통해 client 들에 message 가 전달된다. 

 

그런데 이제 위에 API 선언 및 정의 부분을 확인하면 모두 program.cs 라는 파일에 집중되어있다.

간단한 program 이라면 별 상관 없겠지만 실무에서 쓰인다면 문제가 있는 code 라고 생각된다. 

그래서 이 project 에 vertical slice architecture 를 적용하여 변경해 보겠다.

 

일단 features 라는 폴더를 만들고 push 폴더를 만든 후 여기에 all 과 user 를 처리 하도록 하겠다. 

routes 관련된 내용들도 endpoints 라는 파일로 따로 분리 하겠다. 

 

** 현재 aspnet 7 minimal api 는 router 를 여러파일로 분리한 후 하나로 include 해주는 작업을 지원하지 않는다. 

extension method 를 통해서 처리 하는 방법이 가장 간단한 방법이다.

하지만 다른 방법을 사용해 보겠다. DI 를 이용하여 endpoints 를 분리하고 merging 하겠다.

이 방법은 Carter 라는 dotnet framwork open source 에서 힌트를 얻었다. **

 

자 폴더 구조를 수정해보자

위와 같은 구조로 Features --> Push --> All.cs, Endpoints.cs, User.cs 를 추가하자

 

Endpoints.cs 는 route 정보를 가지고 있다. (Controller 역할)

All.cs 와 User.cs 는 각 기능의 주요 로직을 처리 하는 handler 역할을 한다. 

 

이제 Endpoints.cs 를 구현하기 위한 DI 활용방법을 알아보자.

 

Scrutor 를 이용하여 특정 인터페이스를 구현 또는 상속한 모든 클래스와 인터페이스를 등록하자

dotnet add package Scrutor

Shared --> Injectables --> IInjectable.cs

namespace SignalRTemplate.Shared.Injectables;

public interface IInjectable { }
public interface ITransient : IInjectable { }
public interface IScoped : IInjectable { }
public interface ISingleton : IInjectable { }

 

이제 위 인터페이스들을 DI 에 자동으로 등록되도록 하자

Program.cs

builder.Services.Scan(scan => scan
                               .FromEntryAssembly()
                               .AddClasses(
                                   classes => classes.AssignableTo<ITransient>()
                                )
                               .AsSelfWithInterfaces()
                               .WithTransientLifetime()
                               .AddClasses(
                                   classes => classes.AssignableTo<IScoped>()
                                )
                               .AsSelfWithInterfaces()
                               .WithScopedLifetime()
                               .AddClasses(
                                   classes => classes.AssignableTo<ISingleton>())
                               .AsSelfWithInterfaces()
                               .WithSingletonLifetime()
                               );

 

Shared-->Interfaces-->IEndpoints.cs

public interface IEndpoints:ITransient
{
    void AddEndPoints(IEndpointRouteBuilder routes);
}

 

위 AddEndPoints 에 app.MapGet ... 등을 작업을 구현하면

IEndPoints 를 구현한 모든 class 들을 찾아서 AddEndPoints 를 call 해주면

자동으로 router 에 경로가 등록되게 되는 것이다. 

 

AddEndPoints 를 call 해줄 확장 메소드를 작성하자

Shared-->Extensions-->WebApplicationExtension.cs

using SignalRTemplate.Shared.Interfaces;

namespace SignalRTemplate.Shared.Extensions;

public static class WebApplicationExtension
{
    public static void MapEndpoints(this WebApplication self)
    {
    	// DI 시스템을 이용해 IEndpoints 로 등록된 모든 class 를 찾는다.
        var endpoints = self.Services.GetServices<IEndpoints>();
        foreach (var endpoint in endpoints)
        {
            endpoint.AddEndPoints(self);
        }
    }
}

이제 Program.cs  에 다음출 추가한다. 

app.MapEndpoints();

 

이제 MediaR 을 추가 하자

dotnet add package MediatR

Program.cs 에 mediatR 을 사용한다고 등록하자

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

이제 기본적인 준비는 되었으니 Features 를 구현해 보자

 

Endpoints.cs

using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using SignalRTemplate.Shared.Interfaces;
using SignalRTemplate.SignalR;

namespace SignalRTemplate.Features.Push;
public class Endpoints : IEndpoints
{
    public void AddEndPoints(IEndpointRouteBuilder routes)
    {
        var routeGroup = routes.MapGroup("push").WithOpenApi();

        routeGroup.MapPost("all",
        async ([FromBody] All.Command request, IMediator mediator) => await mediator.Send(request));

        routeGroup.MapPost("user",
        async ([FromBody] User.Command request, IMediator mediator) => await mediator.Send(request));
    }
}

All.cs

using MediatR;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using SignalRTemplate.Shared.Interfaces;
using SignalRTemplate.SignalR;

namespace SignalRTemplate.Features.Push;

public class All
{
    public class Command : IRequest<Response>
    {
        public string Id { get; set; }
        public string Message { get; set; }
    }
    public class Response : BaseResponse
    {
    }

    public class CommandHandler : IRequestHandler<Command, Response>
    {
        private readonly IHubContext<ChannelHub> _hubContext;

        // ITokenGenerator 를 Constructor injection 
        public CommandHandler(IHubContext<ChannelHub> hubContext)
        {
            _hubContext = hubContext;
        }

        public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
        {
            var response = new Response { Result = false };
            await _hubContext.Clients.Group(request.Id).SendAsync("receiveMessage", request.Message);
            response.Result = true;
            return response;
        }
    }

}

User.cs

using MediatR;
using Microsoft.AspNetCore.SignalR;
using SignalRTemplate.Shared.Interfaces;
using SignalRTemplate.SignalR;

namespace SignalRTemplate.Features.Push;

public class User
{
    public class Command : IRequest<Response>
    {
        public string Id { get; set; }
        public string Message { get; set; }
    }
    public class Response : BaseResponse
    {
    }

    public class CommandHandler : IRequestHandler<Command, Response>
    {
        private readonly IHubContext<ChannelHub> _hubContext;

        // ITokenGenerator 를 Constructor injection 
        public CommandHandler(IHubContext<ChannelHub> hubContext)
        {
            _hubContext = hubContext;
        }

        public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
        {
            var response = new Response { Result = false };
            await _hubContext.Clients.Group(request.Id).SendAsync("receiveMessage", request.Message);
            response.Result = true;
            return response;
        }
    }
}

 

이제 실행해보자

(swagger 를 이용할 경우 오류가 날 수 있다. 다음과 같이 program.cs 를 수정하자)

builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "SignalR Template API", Version = "v1" });
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.Http,
        Name = "Bearer",
        BearerFormat = "JWT",
        Description = "Please enter authorization key",
        Scheme = "Bearer"
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement()
                {
                    {
                        new OpenApiSecurityScheme()
                        {
                             Reference = new OpenApiReference()
                             {
                                 Type = ReferenceType.SecurityScheme,
                                 Id = "Bearer"
                             }
                        },
                        Enumerable.Empty<string>().ToList()
                    }
                });
    c.CustomSchemaIds(x => x.FullName?.Replace("+", "."));
});

내부 class 를 인식 못하는 버그가 있으니 위와 같이 수정한 후 실행하자.

c.CustomSchemaIds(x => x.FullName?.Replace("+", "."));   <-- 이라인이 버그를 수정하는 라인이다. 

그외 라인들은 기본정보와 bearer token 을 처리 하기 위한 것이다.

 

 

관련영상

https://youtu.be/IHx35ou6Hpo

반응형