I am using Microsoft.AspNetCore.SignalR nuget package with Bazinga.AspNetCore.Authentication.Basic which adds basic authentication to dotnet core. My C# SignalR client connects when there is no authentication, but when I add AuthorizeAttribute it connects by http and http request header gets authenticated successfully but the Socket does not authenticate probably because there is no header in socket messages.
So I am wondering how should I pass a token or something to authenticated socket connection or is there a example code that I can follow. I think I should pass a random token to just authenticated user and the user needs to constantly pass the token in messages.
Client project, Server project
Server:
using System.Threading.Tasks;
using Bazinga.AspNetCore.Authentication.Basic;
using Domainlogic;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace API
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options => options.AddPolicy("CorsPolicy", builder =>
{
builder
.AllowAnyMethod()
.AllowAnyHeader()
.AllowAnyOrigin();
}));
services.AddSignalR();
services.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme)
.AddBasicAuthentication(credentials => Task.FromResult(
credentials.username == "SomeUserName"
&& credentials.password == "SomePassword"));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("CorsPolicy");
app.UseCors(CorsConstants.AnyOrigin);
app.UseFileServer();
app.UseSignalR(route => { route.MapHub<MessageHub>("/chat"); });
app.UseAuthentication();
}
}
}
Server hub:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Domainlogic
{
public class MessagePayload
{
public string Name { get; set; }
public string Message { get; set; }
public DateTime Date { get; set; }
}
[Authorize]
public class MessageHub : Hub
{
// connected IDs
private static readonly HashSet<string> ConnectedIds = new HashSet<string>();
public override async Task OnConnectedAsync()
{
ConnectedIds.Add(Context.ConnectionId);
await Clients.All.SendAsync("SendAction", "joined", ConnectedIds.Count);
}
public override async Task OnDisconnectedAsync(Exception ex)
{
ConnectedIds.Remove(Context.ConnectionId);
await Clients.All.SendAsync("SendAction", "left", ConnectedIds.Count);
}
public async Task Send(MessagePayload message)
{
await Clients.All.SendAsync("SendMessage", message);
}
}
}
Client:
using System;
using System.Net;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Connections.Client;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
namespace SignalRClient
{
public class MessagePayload
{
public string Name { get; set; }
public string Message { get; set; }
public DateTime Date { get; set; }
}
class Program
{
public static string Base64Encode(string plainText) {
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
return System.Convert.ToBase64String(plainTextBytes);
}
static void Main(string[] args)
{
var credential = Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes("SomeUserName" + ":" + "SomePassword"));
//Set connection
var connection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/chat", options =>
{
options.Headers.Add("Authorization", $"Basic {credential}");
})
.AddJsonProtocol()
.Build();
connection.On<MessagePayload>("SendMessage", param =>
{
Console.WriteLine(param.Message);
});
connection.StartAsync().Wait();
var startTimeSpan = TimeSpan.Zero;
var periodTimeSpan = TimeSpan.FromSeconds(3);
int i = 0;
var timer = new System.Threading.Timer((e) =>
{
connection.InvokeAsync<MessagePayload>("Send", new MessagePayload()
{
Message = "Some message: " + i++
});
}, null, startTimeSpan, periodTimeSpan);
Console.Read();
connection.StopAsync();
}
}
}
Thanks to "davidfowl" on GitHub, the solution was moving UseAuthentication above the UseSignalR.
Source: https://github.com/aspnet/SignalR/issues/2316
Instead of:
app.UseSignalR(route => { route.MapHub<MessageHub>("/chat"); });
app.UseAuthentication();
Use this:
app.UseAuthentication();
app.UseSignalR(route => { route.MapHub<MessageHub>("/chat"); });
To solve this I had to store in server cache with a Dictionary the token of the user with the connectionId. That because in the hub I don't have control over the session.
So every time a user is connected with the hub I exposed and endpoint which receives the connectionId and the token of the user. When a request is handle by the Hub I know by the connection which is the user authenticated.
Controller
[HttpPost]
[Authorize]
[Route("{connectionId}")]
public IActionResult Post(string connectionId)
{
this.hubConnectionService.AddConnection(connectionId, this.workContext.CurrentUserId);
return this.Ok();
}
Hub
public override Task OnDisconnectedAsync(Exception exception)
{
this.hubConnectionService.RemoveConnection(this.Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
Related
I'm trying to create an ActionFilter that will read the Accept-Language header and find if it matches any value in locale and if it does not match, use the a default value which is "en".
First of all, this is my handler:
using MediatR;
using Microsoft.EntityFrameworkCore;
using Subscription.Domain.Common;
using Subscription.Domain.Exceptions;
using Subscription.Infrastructure.Configuration;
using System.Net;
namespace Subscription.API.Application.Package.Queries.Get
{
public class GetHandler : IRequestHandler<GetRequest, EntityResponseModel>
{
private readonly SubscriptionContext _context;
private readonly IHttpContextAccessor _httpContext;
public GetHandler(SubscriptionContext context, IHttpContextAccessor httpContext)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_httpContext = httpContext;
}
public async Task<EntityResponseModel> Handle(GetRequest request, CancellationToken cancellationToken)
{
var lang = _httpContext.HttpContext.Request.Headers["Accept-Language"].ToString();
var packages = await _context.Packages.Where(x => !x.IsDeleted)
.Select(x => new GetResponseModel()
{
Id = x.Id,
BrandId = x.BrandId,
StartAt = x.StartAt,
IconUrl = x.IconUrl,
ImageUrl = x.ImageUrl,
CreatedAt = x.CreatedDate,
Title = x.PackageTranslations.FirstOrDefault(pt => pt.PackageId == x.Id && pt.Locale.Equals(lang)).Title,
Description = x.PackageTranslations.FirstOrDefault(pt => pt.PackageId == x.Id && pt.Locale.Equals(lang)).Description
}).ToListAsync();
if (!packages.Any())
{
throw new DomainException(HttpStatusCode.NotFound, "No record found.");
}
return new EntityResponseModel()
{
Data = packages
};
}
}
}
And this is the controller:
using Microsoft.AspNetCore.Mvc;
using MediatR;
using Subscription.API.Application.Package.Queries.Get;
using Subscription.API.Application.Bundle.Queries.GetById;
using Subscription.API.Filters;
namespace Subscription.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PackagesController : ControllerBase
{
private readonly IMediator _mediator;
public PackagesController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
[ValidateHeaders]
public async Task<ActionResult> GetAll()
{
var response = await _mediator.Send(new GetRequest() { });
return Ok(response);
}
}
}
This the filter class:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Subscription.API.Filters
{
public class ValidateHeaders : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var headers = context.HttpContext.Request.Headers;
// need to do something here
}
}
}
So, basically what I need is to create a filter that:
Check the Accept-Language header if it have a value that matches locale
If it does not have a value that matches it, return string with the default value which is "en"
Any idea what I should do?
You can do this in 2 different ways;
Action Filter
Middleware
1.Action Filter
If you use this action filter, you have to write this attribute on each endpoint.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Subscription.API.Filters
{
public class ValidateHeaders : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var languageHeader = context.HttpContext.Request.Headers.AcceptLanguage;
if (string.IsNullOrWhiteSpace(languageHeader))
context.HttpContext.Request.Headers.AcceptLanguage = "en";
}
}
}
2.Middleware
If you write it as middleware, it will be enough to place this middleware in the first place in the request pipeline.
public class ValidateHeadersMiddleware
{
private readonly RequestDelegate _next;
public ValidateHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var languageHeader = context.Request.Headers.AcceptLanguage;
if (string.IsNullOrWhiteSpace(languageHeader))
context.Request.Headers.AcceptLanguage = "en";
//Continue processing
if (_next != null)
await _next.Invoke(context);
}
}
This will effect all requests.
public static class MiddlewareExtension
{
public static IApplicationBuilder UseHeaderValidation(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ValidateHeadersMiddleware>();
}
}
In the Configure method in Startup.cs or minimal api configuration in Program.cs, use this middleware;
...
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHeaderValidation();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
...
I have an Azure SQL DB that initially had the following columns:
user name
password hash
password salt
This DB serves a .NET Core C# API that checks username and password to return a JWT token.
The API had a User object that comprised all three columns with the correct types, a DbContext with a DbSet<User>, and an IServiceCollection that used said DbContext.
The API worked fine, returning a JWT token as needed.
I have since needed to add an extra parameter to check and pass to the JWT creation - the relevant column has been created in the DB, the User object in the API has been updated to include the extra parameter and that extra parameter is observed in the Intellisense throughout the API code.
The issue is that when the API is deployed to Azure, the extra parameter isn't being recognised and populated; how do I make the API correctly update to use the new DbContext and retrieve the User with the extra parameter?
(I've omitted the interfaces for brevity, as they're essentially the corresponding classes)
User, UserRequest and MyApiDbContext Classes:
using Microsoft.EntityFrameworkCore;
namespace MyApi.Models
{
// Basic user model used for authentication
public class User
{
public string UserId { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public string ExtraParam { get; set; } // newly added parameter
}
public class UserRequest
{
public string UserId { get; set; }
public string password { get; set; }
}
public class MyApiDbContext : DbContext
{
public MyApiDbContext(DbContextOptions<MyApiDbContext> options)
: base(options)
{
}
public DbSet<User> Users { get; set; }
}
}
The AuthRepository that retrieves the user:
using Microsoft.EntityFrameworkCore;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
namespace MyApi.Services
{
public class AuthRepository : IAuthRepository
{
private readonly MyApiDbContext _context;
public AuthRepository(MyApiDbContext context)
{
_context = context;
}
public async Task<User> Login(string username, string password)
{
// my test user gets returned
User returnedUser = await _context.Users.FirstOrDefaultAsync(x => x.UserId == username);
if (returnedUser == null)
{
return null;
}
// the password get verified
if (!VerifyPasswordHash(password, returnedUser.PasswordHash, returnedUser.PasswordSalt))
{
return null;
}
// this does not get changed, but the value set in the DB is definitely a string
if (returnedUser.ExtraParam == null || returnedUser.ExtraParam == "")
{
returnedUser.ExtraParam = "placeholder"
}
return returnedUser;
}
}
}
The AuthService that calls the AuthRepository for the user then "creates the JWT token" (just returning a string for this example), currently set up to return the user details:
using Microsoft.Extensions.Options;
using MyApi.Interfaces;
using MyApi.Models;
using System;
using System.Threading.Tasks;
namespace MyApi.Services
{
public class AuthService : IAuthService
{
private readonly IOptions<MyApiBlobStorageOptions> _settings;
private readonly IAuthRepository _repository;
public AuthService(IOptions<MyApiBlobStorageOptions> settings, IAuthRepository repository)
{
_repository = repository;
_settings = settings;
}
public async Task<string> Login(string username, string password)
{
User returnedUser = await _repository.Login(username, password);
if (returnedUser != null)
{
// currently returns "UserIdInDB,ProvidedPasswordFromLogin,"
return $"{returnedUser.UserId},{password},{returnedUser.ExtraParam}";
}
return null;
}
}
}
The controller that calls the AuthService:
using Microsoft.AspNetCore.Mvc;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace MyApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly MyApiDbContext _context;
private readonly IAuthService _authService;
public AuthController(MyApiDbContext context, IAuthService authService)
{
_context = context;
_authService = authService;
}
[HttpPost("login")]
public async Task<IActionResult> Login(UserRequest loginUser)
{
string token = await _authService.Login(loginUser.UserId, loginUser.Password);
if (token != null)
{
return Ok(token);
}
return Unauthorized("Access Denied!!");
}
}
}
The startup class that registers everything:
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MyApi.Interfaces;
using MyApi.Models;
using MyApi.Services;
using Microsoft.Extensions.Azure;
using Azure.Storage.Queues;
using Azure.Storage.Blobs;
using Azure.Core.Extensions;
using System;
namespace MyApi
{
public class Startup
{
public IConfiguration Configuration { get; }
private readonly ILogger<Startup> _logger;
private readonly IConfiguration _config;
public Startup(ILogger<Startup> logger, IConfiguration config)
{
_logger = logger;
_config = config;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add dBContext for DB
services.AddDbContextPool<MyApiDbContext>(options => options.UseSqlServer(_config.GetConnectionString("MyAzureDb")));
// Add DI Reference for Repository
services.AddScoped<IAuthRepository, AuthRepository>();
// Add DI Reference for Azure Blob Storage Processes
services.AddScoped<IBlobService, AzureBlobService>();
// DI Reference for AuthService
services.AddScoped<IAuthService, AuthService>();
// Add configuration section for Constructor Injection
services.Configure<ApiBlobStorageOptions>(_config.GetSection("MyApiBlobStorage"));
services.AddMvc(mvcOptions => mvcOptions.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Latest);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
.GetBytes(_config.GetSection("MyApiBlobStorage:Secret").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
_logger.LogWarning("Token authentication failed whilst attempting to upload file");
return Task.CompletedTask;
}
};
});
services.AddAzureClients(builder =>
{
builder.AddBlobServiceClient(Configuration["ConnectionStrings:MyApiBlobStorage/AzureBlobStorageConnectionString:blob"], preferMsi: true);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseAuthentication();
app.UseMvc();
}
}
internal static class StartupExtensions
{
public static IAzureClientBuilder<BlobServiceClient, BlobClientOptions> AddBlobServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
{
if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
{
return builder.AddBlobServiceClient(serviceUri);
}
else
{
return builder.AddBlobServiceClient(serviceUriOrConnectionString);
}
}
public static IAzureClientBuilder<QueueServiceClient, QueueClientOptions> AddQueueServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
{
if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
{
return builder.AddQueueServiceClient(serviceUri);
}
else
{
return builder.AddQueueServiceClient(serviceUriOrConnectionString);
}
}
}
}
Let me know if there is anything else required for understanding: the only difference between before and now is the addition of ExtraParam and the corresponding references throughout for the API, and the DB getting the identically named column.
I tried adding the parameter and deploying it to Azure and making the POST request as normal, starting and stopping the app service, deploying the API while the app service was stopped and starting it again, and restarting the app service. I don't know how much I could try changing up what I'm doing, I'm trying to do exactly the same as before, but with an extra parameter getting requested from the DB.
I can also confirm that the DB contains the ExtraParam column, and that it contains values against the existing data rows, as viewed using the Azure Portal's DB Query Editor.
I've resolved the issue, partially because of posting this question and sanitising the code for public discussion.
In the Login Controller, in my development code the request for the user to be returned was subsequently ignored, passing through the user request details which had a null ExtraParam, not the returned user which had the ExtraParam populated.
The moral of the story is to confirm which objects are being used at which points in the code, or have one object that is passed into, updated by, then returned from functions to maintain consistency.
I'm trying to set up global exception handling code in .NetCore 3.1 webpai
My goal is to log unhandled exception before the app exits, using log4net.
I tried following several tutorials, one using a filter, and several using middelware and when I'm done and test it the middleware never gets called when I throw an exception thusly.
I have a filter already (which is commented out for testing the middle ware in case they were interacting) which does work, but can't use IOC to load an instanve of ILogger
[HttpGet]
[Route( "/ThrowException" )]
public JqGridReturnCV ThrowException()
{
log.LogTrace( "AdStudentController::ThrowException() - in" );
throw new Exception( "This is a test Exception" );
log.LogTrace( "AdStudentController::ThrowException() - out" );
}
Here is my code for the middleware:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using log4net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using StudentPortal4Api.Dto;
namespace StudentPortal4Api.Utilities
{
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate next;
public readonly ILogger log;
public GlobalExceptionMiddleware( RequestDelegate _next, ILogger _log )
{
next = _next;
log = _log;
}
public async Task Invoke( HttpContext context )
{
try
{
await next( context );
}
catch ( Exception ex )
{
var response = context.Response;
response.ContentType = "application/json";
switch ( ex )
{
default:
// unhandled error
log.Log( LogLevel.Error, " GlobalException:" + ex.ToString() );
break;
}
throw;
}
}
}
public class ErrorDetails
{
public int StatusCode { get; set; }
public string Message { get; set; }
public override string ToString()
{
return JsonConvert.SerializeObject( this );
}
}
public static class ExceptionMiddlewareExtensions
{
public static void ConfigureExceptionHandler( this IApplicationBuilder app, ILogger logger )
{
app.UseExceptionHandler( appError =>
{
appError.Run( async context =>
{
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
if ( contextFeature != null )
{
logger.LogError( $"Something went wrong: {contextFeature.Error}" );
await context.Response.WriteAsync( new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = "Internal Server Error."
}.ToString() );
}
} );
} );
}
}
}
and here is my configure method , where I suspect I'm doing something wrong in registering it
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure( IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, ILogger log )
{
//code removed for clarity
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
//Code removed for clarity
// global error handler
app.UseMiddleware<GlobalExceptionMiddleware>();
app.ConfigureExceptionHandler( log );
}
}
}
anyone see what I'm doing wrong?
You have a order issue source
change to:
app.UseAuthorization();
// global error handler
app.UseMiddleware<GlobalExceptionMiddleware>(); //custom Middleware Must be before endpoints and after auth.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
You will not need the app.UseExceptionHandler.
Im using the following code to generate a link for Email Authentication ( user should click on the link and if it was valid , the account would be activarted) . the following code would work correctly in the controller but i refactured the code and moved it to the Service class . Request.Schema DOES'NT RECOGNIZE CORRECT NAMESPACE . i have tried several ways and packages but its not working . how can i Solve it ?
usings :
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
//using Microsoft.AspNetCore.Mvc;
using ProjectName.Core.DTOs.ClientDTOs;
using ProjectName.Core.Services.Interfaces;
using ProjectName.Core.Utilities;
using ProjectName.DataLayer.Context;
using ProjectName.DataLayer.Entities.PublicEntities;
using ProjectName.DataLayer.Entities.UserEntities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
//using System.Web.Mvc;
using System.Threading.Tasks;
I followed this document on microsoft but still not working....
Url.Action
var address = Microsoft.AspNetCore.Mvc.IUrlHelper.Action(
"ConfirmEmail",
"Account",
new { username = newUser.UserName, token = emailConfirmationToken },
**Request.Scheme**);
at first make a class for encoding urls and tokens.
public class TokenUrlEncoderService
{
public string EncodeToken(string token)
{
return WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(token));
}
public string DecodeToken(string urlToken)
{
return Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(urlToken));
}
}
then make a class in your service layer like this.
public class IdentityEmailService
{
public IdentityEmailService(IEmailSender sender,
UserManager<IdentityUser> userMgr,
IHttpContextAccessor contextAccessor,
LinkGenerator generator,
TokenUrlEncoderService encoder)
{
EmailSender = sender;
UserManager = userMgr;
ContextAccessor = contextAccessor;
LinkGenerator = generator;
TokenEncoder = encoder;
}
public IEmailSender EmailSender { get; set; }
public UserManager<IdentityUser> UserManager { get; set; }
public IHttpContextAccessor ContextAccessor { get; set; }
public LinkGenerator LinkGenerator { get; set; }
public TokenUrlEncoderService TokenEncoder { get; set; }
private string GetUrl(string emailAddress, string token, string page)
{
string safeToken = TokenEncoder.EncodeToken(token);
return LinkGenerator.GetUriByPage(ContextAccessor.HttpContext, page,
null, new { email = emailAddress, token = safeToken });
}
}
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-5.0#use-httpcontext-from-custom-components
the following code would work correctly in the controller but i refactured the code and moved it to the Service class .
Request here is property in ControllerBase class, your service class must inherit from ControllerBase or Controller.
Change your code like below:
public interface IGenerateUrl
{
string GetUrl();
}
public class GernateUrl : IGenerateUrl
{
private readonly IUrlHelper url;
public GernateUrl(IUrlHelper url)
{
this.url = url;
}
public string GetUrl()
{
string scheme = url.ActionContext.HttpContext.Request.Scheme;
var address = url.Action(
"ConfirmEmail",
"Account",
new { username = "user", token = "token" },
scheme);
return address;
}
}
Be sure register the service like below:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped<IGenerateUrl, GernateUrl>();
services.AddScoped<IUrlHelper>(x =>
{
var actionContext = x.GetRequiredService<IActionContextAccessor>().ActionContext;
var factory = x.GetRequiredService<IUrlHelperFactory>();
return factory.GetUrlHelper(actionContext);
});
services.AddControllersWithViews();
}
I am attempting to send a stream using RabbitMQ and Servicestack (v1.0.41 using .NET Core).
My Request implements ServiceStack.Web.IRequiresRequestStream, and the stream property is set in the client, but when it gets to the server, the stream is NULL.
Complete Repo
Server Code:
using System;
using System.IO;
using System.Threading.Tasks;
using Funq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using ServiceStack;
using ServiceStack.Messaging;
using ServiceStack.RabbitMq;
using ServiceStack.Web;
namespace Server
{
class Program
{
public static void Main(string[] args)
{
IWebHost host = new WebHostBuilder()
.UseServer(new RabbitServer())
.UseStartup<Startup>()
.Build();
host.Run();
}
}
public class RabbitServer : IServer
{
public void Dispose(){}
public void Start<TContext>(IHttpApplication<TContext> application){}
public IFeatureCollection Features { get; } = new FeatureCollection();
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseServiceStack((AppHostBase)Activator.CreateInstance<AppHost>());
app.Run((RequestDelegate)(context => (Task)Task.FromResult<int>(0)));
}
}
public class AppHost : AppHostBase
{
public AppHost()
: base("My Test Service", typeof(MyService).GetAssembly())
{
}
public override void Configure(Container container)
{
var mqServer = new RabbitMqServer("127.0.0.1");
container.Register<IMessageService>(mqServer);
mqServer.RegisterHandler<HelloRequest>(ExecuteMessage);
mqServer.Start();
}
}
public class MyService : Service
{
public HelloResponse Any(HelloRequest request)
{
Console.WriteLine($"Stream is null: {request.RequestStream == null}");
return new HelloResponse { Counter = request.Counter };
}
}
public class HelloRequest : IReturn<HelloResponse>, IRequiresRequestStream
{
public int Counter { get; set; }
public Stream RequestStream { get; set; }
}
public class HelloResponse
{
public int Counter { get; set; }
}
}
Client Code:
using ServiceStack;
using ServiceStack.Messaging;
using ServiceStack.RabbitMq;
using ServiceStack.Web;
using System;
using System.IO;
using System.Text;
namespace Client
{
class Program
{
static void Main(string[] args)
{
RabbitMqServer messageService = new RabbitMqServer("127.0.0.1");
RabbitMqQueueClient mqClient = messageService.MessageFactory.CreateMessageQueueClient() as RabbitMqQueueClient;
var responseQueueName = mqClient.GetTempQueueName();
MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")) { Position = 0 };
HelloRequest request = new HelloRequest { Counter = 100, RequestStream = ms }; //Counter is just some arbitary extra data
Guid messageId = Guid.NewGuid();
mqClient.Publish(QueueNames<HelloRequest>.In, new Message<HelloRequest>(request) { ReplyTo = responseQueueName, Id = messageId });
}
}
public class HelloRequest : IReturn<HelloResponse>, IRequiresRequestStream
{
public int Counter { get; set; }
public Stream RequestStream { get; set; }
}
public class HelloResponse
{
public int Counter { get; set; }
}
}
Note: I realise I could just use a byte[] in my request object, but I would quite like to make use of the provided IRequiresRequestStream interface so I can switch back to using HTTP rather than AMQP in the future.
I should also say, that I probably won't be using the RabbitMQ Client provided by servicestack, as I am writing custom logic to convert from HTTP to AMQP, so I will be building the rabbitMQ request manually - the code above just demonstrates the problem I am having in the simplest way possible.
I'm going to assume that this won't just work out of the box with AMQP (as it does with HTTP) - so I was thinking that I need to do something like serialize the stream to a byte[] and include it in the RabbitMQ message and then populate the dto which ServiceStack magically re-hydrates on the Server.
So two questions really...
1. Am I on the right track?
2. If so, how do I hook into the de-serialization code on the server so that I have access to the raw RabbitMQ message in order to convert my byte[] back to a stream and set the stream on my dto?
You can't send a IRequiresRequestStream Request DTO into a MQ because it's not a normal serialized Request DTO, instead it instructs ServiceStack to skip deserializing the Request DTO and instead inject the HTTP Request Stream so the Service can perform its own Deserialization instead, this is different to a normal Request DTO which is serialized and can be sent as the body in an MQ Message.
One option if you want to share implementation between a IRequiresRequestStream Service and a Service that can be called by MQ is to just delegate to a common Service that accepts bytes, e.g:
//Called from HTTP
public object Any(HelloStream request) =>
Any(new HelloBytes { Bytes = request.RequestStream.ReadFully() });
//Called from HTTP or MQ
public object Any(HelloBytes request)
{
//= request.Bytes
}