How to get response via Masstransit? - rabbitmq

I want to get a response (GetStatusResponse) from consumer (GetStatusConsumer).
My request is putted in Rabbit queue "getstatus" but my consumer is not rise and timeout occurs.
Publish-method and Consumer nested in one project
It seems to me trouble in the Startup.cs. Could you help me?
I have following code in Startup.cs
...
services.AddSingleton(provider =>
{
var getStatusBusOptions = provider.GetRequiredService<IOptions<BusOptions>>().Value;
var bus = Bus.Factory.CreateUsingRabbitMq(sbc =>
{
var host = sbc.Host(new Uri(getStatusBusOptions.HostUri), h =>
{
h.Username(getStatusBusOptions.UserName);
h.Password(getStatusBusOptions.Password);
});
sbc.ReceiveEndpoint("getstatus", ep =>
{
ep.Consumer<GetStatusConsumer>(provider);
ep.PrefetchCount = getStatusBusOptions.PrefetchCount;
ep.UseConcurrencyLimit(getStatusBusOptions.UseConcurrencyLimit);
});
});
return new GetStatusBus
{
Bus = bus,
HostUri = getStatusBusOptions.HostUri
};
});
...
Following code in class GetStatusPublisher.cs
public class GetStatusPublisher : IGetStatusPublisher
{
readonly GetStatusBus _bus;
public GetStatusPublisher(GetStatusBus bus)
{
_bus = bus;
}
public async Task<Tout> GetResponse<Tin, Tout>(Tin request) where Tin : class where Tout : class
{
var serviceAddress = new Uri($"rabbitmq://rabbitmq.test.com/jgt/getstatus");
var timeout = TimeSpan.FromSeconds(30);
var client = new MessageRequestClient<Tin, Tout>(_bus.Bus, serviceAddress, timeout);
var resp = await client.Request(request); // <== Timeout here and don't rise consumer (GetStatusConsumer)
return resp;
}
Here is Publish-method:
...
readonly IGetStatusPublisher _getStatusPublisher;
...
var resp = await _getStatusPublisher.GetResponse<GetStatusRequest, GetStatusResponse>(statusReq);
Consumer has following code:
public class GetStatusConsumer : MetricWriter, IConsumer<GetStatusRequest>
{
public GetStatusConsumer(IMetrics metrics) : base(metrics)
{
......
}
public async Task Consume(ConsumeContext<GetStatusRequest> context)
{
....
}
}

First things first, I don't think you're starting the bus. That's the major issue.
However...
Are you using an antique version of MassTransit? Your code is seriously out of date, and I'd suggest updating it to use the current version/syntax as your code example above has so many things wrong with it.
services.AddMassTransit(x =>
{
x.AddConsumer<GetStatusConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
var getStatusBusOptions = context.GetRequiredService<IOptions<BusOptions>>().Value;
cfg.Host(new Uri(getStatusBusOptions.HostUri), h =>
{
h.Username(getStatusBusOptions.UserName);
h.Password(getStatusBusOptions.Password);
});
cfg.PrefetchCount = getStatusBusOptions.PrefetchCount;
cfg.ConcurrentMessageLimit = getStatusBusOptions.UseConcurrencyLimit;
cfg.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService();
services.AddGenericRequestClient();
Then, you can simply add a dependency on IRequestClient<T> to send requests and get responses. Your updated publisher code may look like:
public class GetStatusPublisher :
IGetStatusPublisher
{
public GetStatusPublisher(IServiceProvider provider)
{
_provider = provider;
}
public async Task<Tout> GetResponse<Tin, Tout>(Tin request) where Tin : class where Tout : class
{
var client = _provider.GetRequiredService<IRequestClient<Tin>>();
var response = await client.GetResponse<Tout>(request);
return response.Message;
}
}

If I use following code I get error: "System.InvalidOperationException: Cannot resolve scoped service 'MassTransit.IRequestClient`1[GetStatusTry.Contracts.GetStatusRequest]' from root provider....."
public async Task<Tout> GetResponse<Tin, Tout>(Tin request) where Tin : class where Tout : class
{
try
{
var client = _provider.GetRequiredService<IRequestClient<Tin>>(); // << --- here is error
var response = await client.GetResponse<Tout>(request);
return response.Message;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
}
But this code works Ok:
public class Dev2Controller : ControllerBase
{
IRequestClient<GetStatusRequest> _client;
public Dev2Controller(IGetStatusPublisher getStatusPublisher, IRequestClient<GetStatusRequest> client)
{
_client = client;
}
[HttpPost]
public async Task<GetStatusResponse> GetStatus2()
{
var req = new GetStatusRequest { Statuses = new List<int> { 1, 2, 3 }, TerminalDescr = "try to get status2" };
var response = await _client.GetResponse<GetStatusResponse>(req);
return (response.Message);
}
}

this code is working
public class GetStatusPublisher : IGetStatusPublisher
{
readonly IServiceProvider _provider;
public GetStatusPublisher(IServiceProvider provider)
{
_provider = provider;
}
public async Task<Tout> GetResponse<Tin, Tout>(Tin request) where Tin : class where Tout : class
{
try
{
using (var _scope = _provider.CreateScope())
{
var client = _scope.ServiceProvider.GetRequiredService<IRequestClient<Tin>>();
var response = await client.GetResponse<Tout>(request);
return response.Message;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
}
}

Related

Asp.net Core 3.1 Web Api return custom error message from IActionResult

Is it possible to return custom error messages to client from Asp.Net Core 3.1 Web Api? I've tried a few different things to set the "ReasonPhrase" with no luck. I have tried using StatusCode:
return StatusCode(406, "Employee already exists");
I tried to return using HttpResponseMessage:
HttpResponseMessage msg = new HttpResponseMessage();
msg.StatusCode = HttpStatusCode.NotAcceptable;
msg.ReasonPhrase = "Employee alredy exists";
return (IActionResult)msg;
I am trying to return a message to the client calling the method that the employee already exists:
public async Task<IActionResult> CreateEmployee([FromBody] EmployeeImport Employee)
{
var exists = await employeeService.CheckForExistingEmployee(Employee);
if (exists > 0)
{
//return StatusCode(406, "Employee already exists");
HttpResponseMessage msg = new HttpResponseMessage();
msg.StatusCode = HttpStatusCode.NotAcceptable;
msg.ReasonPhrase = "Employee already exists";
return (IActionResult)msg;
}
}
This is the code in the client:
public async Task<ActionResult>AddEmployee(EmployeeImport employee)
{
var message = await CommonClient.AddEmployee(employee);
return Json(message.ReasonPhrase, JsonRequestBehavior.AllowGet);
}
public async Task<HttpResponseMessage> AddEmployee(EmployeeImport employee)
{
var param = Newtonsoft.Json.JsonConvert.SerializeObject(employee);
HttpContent contentPost = new StringContent(param, System.Text.Encoding.UTF8, "application/json");
var response = await PerformPostAsync("entity/NewEmployee", contentPost);
return response;
}
protected async Task<HttpResponseMessage> PerformPostAsync(string requestUri, HttpContent c)
{
_webApiClient = new HttpClient { BaseAddress = _baseAddress };
_webApiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
var webApiResponse = await _webApiClient.PostAsync(requestUri, c);
return webApiResponse;
}
To do this, you can create a Custom Error class that implements the IActionResult interface as follows:
public class CustomError : IActionResult
{
private readonly HttpStatusCode _status;
private readonly string _errorMessage;
public CustomError(HttpStatusCode status, string errorMessage)
{
_status = status;
_errorMessage = errorMessage;
}
public async Task ExecuteResultAsync(ActionContext context)
{
var objectResult = new ObjectResult(new
{
errorMessage = _errorMessage
})
{
StatusCode = (int)_status,
};
context.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = _errorMessage;
await objectResult.ExecuteResultAsync(context);
}
}
And use the following form :
[HttpGet]
public IActionResult GetEmployee()
{
return new CustomError(HttpStatusCode.NotFound, "The employee was not found");
}
Try changing
return (IActionResult)msg;
to
return Task.FromResult(BadRequest(msg) as IActionResult);

How do you manage the visible input fields accepted in an API HttpPost request?

In my API I have a Create method in my controller that accepts all of the models fields, but in the method I'm excluding the ID field since on a create it's generated. But in Swagger it's showing the following.
Is there a way for it not to show the following part?
"id": 0
Is a viewmodel how I should go about this?
I tried the following, but can't get it to work.
public class PartVM
{
public string Name { get; set; }
}
public interface IPartService
{
Task<Part> CreatePart(PartVM part);
Task<IEnumerable<Part>> GetParts();
Task<Part> GetPart(int partId);
}
public class PartService : IPartService
{
private readonly AppDbContext _appDbContext;
public PartService(AppDbContext appDbContext)
{
_appDbContext = appDbContext;
}
public async Task<Part> CreatePart(PartVM part)
{
var _part = new Part()
{
Name = part.Name
};
var result = await _appDbContext.Parts.AddAsync(_part);
await _appDbContext.SaveChangesAsync();
return result.Entity;
}
}
Here's my controller.
[Route("api/[controller]")]
[ApiController]
public class PartsController : ControllerBase
{
private readonly IPartService _partService;
public PartsController(IPartService partService)
{
_partService = partService;
}
[HttpPost]
public async Task<ActionResult<Part>> CreatePart(PartVM part)
{
try
{
if (part == null)
return BadRequest();
var _part = new Part()
{
Name = part.Name
};
var createdPart = await _partService.CreatePart(_part);
return CreatedAtAction(nameof(GetPart),
new { id = createdPart.Id}, createdPart);
}
catch (Exception /*ex*/)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Error creating new record in the database");
}
}
I'm getting a build error saying "CS1503 Argument 1: cannot convert from 'MusicManager.Shared.Part' to 'MusicManager.Server.Data.ViewModels.PartVM'".
It's refering to "_part" in this line "var createdPart = await _partService.CreatePart(_part);".
Any help is appreciated, thank you!
you have a CreatePart method which receives a PartVM model, but you are sending a Part Model to it
change your method to this :
public async Task<Part> CreatePart(Part part)
{
var result = await _appDbContext.Parts.AddAsync(_part);
await _appDbContext.SaveChangesAsync();
return result.Entity;
}

SSE Eventsource Asp.net Core 3.1

[ApiController]
[Route("/SSE/[action]")]
public class SSEController : Controller
{
private static ConcurrentBag<StreamWriter> clients;
static SSEController()
{
clients = new ConcurrentBag<StreamWriter>();
}
[HttpPost]
public async Task SSECallbackMsg()
{
await CallbackMsg("test");
}
private async Task CallbackMsg(string test)
{
foreach (var client in clients)
{
try
{
var data = string.Format(test);
await client.WriteAsync(data);
await client.FlushAsync();
client.Dispose();
}
catch (Exception)
{
StreamWriter ignore;
clients.TryTake(out ignore);
}
}
}
[HttpGet]
public HttpResponseMessage GETSubscibe()
{
HttpResponseMessage response = new HttpResponseMessage();
response.Content = new PushStreamContent((a, b, c) =>
{ OnStreamAvailable(a, b, c); }, "text/event-stream");
return response;
}
private void OnStreamAvailable(Stream stream, HttpContent content,
TransportContext context)
{
var client = new StreamWriter(stream,Encoding.UTF8);
clients.Add(client);
}
}
Javascript Method of calling above is like
function listenForServerEvents() {
var source = new EventSource('https://localhost:5002/SSE/GETSubscibe');
source.addEventListener("open", function (event) {
console.log('onopen');
}, false);
source.addEventListener("error", function (event) {
if (event.eventPhase == EventSource.CLOSED) {
source.close();
}
}, false);
source.addEventListener("message", function (event) {
console.log('onmessage: ' + event.data);
}, false);
}
when executing, above js function, i am getting error as EventSource's response has a MIME type ("application/json") that is not "text/event-stream". Aborting the connection.
Should add anything in startup.cs or is there any mistake., If anyone knows ,kindly help

Endpoint is null when accessed in middleware during asp.net core 3.1 integration test

I run integration tests for my asp.net core application, the call passes from multiple middle-wares but stops at one of them which has the following line :
var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint;
var attribute = endpoint?.Metadata.GetMetadata<AllowAHeader>();
The endpoint is null.
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
where TStartup : class
{
protected override IHostBuilder CreateHostBuilder()
{
var builder = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(x =>
{
x.UseStartup<TStartup>().UseTestServer();
});
return builder;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
builder.ConfigureTestServices(services =>
{
services.RemoveAll<DbContext>();
services.RemoveAll<DbContextOptions>();
foreach (var option in services.Where(s =>
s.ServiceType.BaseType ==
typeof(DbContextOptions)).ToList())
{
services.Remove(option);
}
services.AddDbContext<DbContext>(options =>
{
options.UseInMemoryDatabase("Testing");
});
});
}
}
Here is the test
public class ClientTests : IClassFixture<CustomWebApplicationFactory<TestStartup>>
{
private readonly HttpClient _client;
public ClientTests(CustomWebApplicationFactory<TestStartup> customWebApplicationFactory)
{
_client = customWebApplicationFactory.CreateClient();
}
[Fact]
public async Task GetClients()
{
_client.DefaultRequestHeaders.Add("X-Integration-Testing", "True");
_client.DefaultRequestHeaders.Add("X-Integration-Authroize", "Basic");
var result = await _client.PostAsync("v1/client", null);
}
}
The TestStartup :
public class TestStartup : Startup
{
public TestStartup(IConfiguration configuration)
: base(configuration)
{
}
protected override void ConfigureMiddlewareForIntegrationTest(IApplicationBuilder app)
{
app.UseMiddleware<AuthenticatedTestRequestMiddleware>();
}
}
public class AuthenticatedTestRequestMiddleware
{
public const string TestingHeader = "X-Integration-Testing";
public const string TestingHeaderAuthValueValue = "X-Integration-Authroize";
private readonly RequestDelegate _next;
public AuthenticatedTestRequestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Headers.Keys.Contains(TestingHeader))
{
if (context.Request.Headers.Keys.Contains(TestingHeaderAuthValueValue))
{
var encoded = "Basic " + System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes("user" + ":" + "123456"));
context.Request.Headers.Add("Authorization", encoded);
}
}
}
}
In ConfigureWebHostDefaults add:
x.UseHttpSys(opt =>
opt.RequestQueueMode = RequestQueueMode.Create;
)
Have not figured out exactly why it's needed, but I'm guessing it's a bug being the value of RequestQueueMode is 0 by default, same as RequestQueueMode.Create's value.

FluentValidation failure not returning BadRequest

I have wired up FluentValidation as per instructions, and when debuging test I can see that model is invalid based on the test setup, but exception is not thrown, but rather method on the controller is being executed. This is on 3.1 with EndPoint routing enabled. Is there anything else one needs to do to get this to work and throw. What happens is that validation obviously runs; it shows as ModelState invalid and correct InstallmentId is invalid, but it keeps processing in Controller instead of throwing exception.
services.AddMvc(
options =>
{
options.EnableEndpointRouting = true;
//// options.Filters.Add<ExceptionFilter>();
//// options.Filters.Add<CustomerRequestFilter>();
})
.AddFluentValidation(
config =>
{
config.RegisterValidatorsFromAssemblyContaining<Startup>();
})
Command and Validator
public class ProcessManualPayment
{
public class Command
: CustomerRequest<Result?>
{
public Guid PaymentPlanId { get; set; }
public Guid InstallmentId { get; set; }
public Guid PaymentCardId { get; set; }
}
public class Validator : AbstractValidator<Command>
{
public Validator()
{
this.RuleFor(x => x.CustomerId)
.IsValidGuid();
this.RuleFor(x => x.PaymentPlanId)
.IsValidGuid();
this.RuleFor(x => x.InstallmentId)
.IsValidGuid();
this.RuleFor(x => x.PaymentCardId)
.IsValidGuid();
}
}
Controller
[Authorize]
[HttpPost]
[Route("payments")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ProcessManualPayment(
[FromBody]
ProcessManualPayment.Command command)
{
Test
[Fact]
public async Task When_Command_Has_Invalid_Payload_Should_Fail()
{
var client = this.factory.CreateClient();
// Arrange
var validCmd = new ProcessManualPayment.Command()
{
CustomerId = Guid.NewGuid(),
PaymentPlanId = Guid.NewGuid(),
InstallmentId = Guid.NewGuid(),
PaymentCardId = Guid.NewGuid(),
};
var validCmdJson = JsonConvert.SerializeObject(validCmd, Formatting.None);
var jObject = JObject.Parse(validCmdJson);
jObject["installmentId"] = "asdf";
var payload = jObject.ToString(Formatting.None);
// Act
var content = new StringContent(payload, Encoding.UTF8, MediaTypeNames.Application.Json);
var response = await client.PostAsync(MakePaymentUrl, content);
var returned = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
[Fact]
public async Task When_Payload_Is_Null_Should_Fail()
{
// Arrange
var client = this.factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(MakePaymentUrl, null);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
GuidValidator
public class GuidValidator : PropertyValidator
{
public GuidValidator()
: base("'{PropertyName}' value {AttemptedValue} is not a valid Guid.")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
context.MessageFormatter.AppendArgument("AttemptedValue", context.PropertyValue ?? "'null'");
if (context.PropertyValue == null)
{
return false;
}
Guid.TryParse(context.PropertyValue.ToString(), out var value);
return IsValid(value);
}
private static bool IsValid(Guid? value) =>
value.HasValue
&& !value.Equals(Guid.Empty);
}
Mystery solved, I was missing [ApiController] attribute on the controller.