.Net Core TestClient cannot post with parameters - integration

I am creating Web API integration test with MSTest Test Server. I can request data. However when I post with data, request routed to Test Server without parameters. Below is my codes, where I am wrong?
/// <summary>
///
/// </summary>
/// <returns></returns>
protected async Task TestDataInitializeAsync()
{
_factory = new WebApplicationFactory<MyWebApi.Startup>();
_client = _factory.CreateClient();
_client.DefaultRequestHeaders.Clear();
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
_client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("username", "userABC"),
new KeyValuePair<string, string>("password","password123")
});
var response = await _client.PostAsync("/account/login", content);
}
Here is my Controller method:
/// <summary>
/// Account controller ASP.NET identity authentication
/// </summary>
[Produces("application/json")]
[Route("[controller]")]
[ApiController]
[Authorize]
public class AccountController : ControllerBase
{
[HttpPost]
[AllowAnonymous]
[Route("login")]
public async Task<object> Login(string userName, string password)
{
try
{
var result = await _signInManager.PasswordSignInAsync(userName, password, false, false);
if (result.Succeeded)
{
var appUser = _userManager.Users.SingleOrDefault(r => r.UserName == userName);
return GenerateJwtToken(userName, appUser);
}
else
{
return BadRequest(new { message = "No user found! Please check user name and password." });
}
}
catch (Exception ex)
{
return StatusCode(Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, ex.Message);
}
}
}
}
Parameters, username and password always null.

You should edit your controller's action to get post parameters from your request's body using FromBody :
[HttpPost]
[AllowAnonymous]
[Route("login")]
public async Task<object> Login([FromBody]LoginVm model)
{
...
}
With the LoginVm class containing Username and Password as string properties.
Hope it helps.

I know the reason after read ASP.NET Core 2.2 Microsoft Document carefully. The key point is [ApiController] attribute. It does not related with TestClient.
In .NET Core 2.2, if you put [ApiController] attribute in Class level or Assembly level, .NET Core framework will do parameter binding automatically. We no need to define as [FromBody] in front of the object parameters for complex type. However, for simple type such as int, string, bool, we need to put [FromBody], [FromForm], [FromQuery], [FromHeader], [FromRoute], [FromServices], accordingly.
In my case, I just put [FromFrom] attributes in front of two simple type parameters as below and problem was solved.
<summary>
/// To log-in to the system by using userName and password.
/// If authentication is successed, bearer token will response from server.
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <returns>Bearer Token</returns>
/// <remarks>
/// </remarks>
[HttpPost]
[AllowAnonymous]
[Route("login")]
public async Task<object> Login([FromForm] string userName, [FromForm] string password)
{
try
{
var result = await _signInManager.PasswordSignInAsync(userName, password, false, false);
if (result.Succeeded)
{
var appUser = _userManager.Users.SingleOrDefault(r => r.UserName == userName);
return GenerateJwtToken(userName, appUser);
}
else
{
return BadRequest(new { message = "No user found! Please check user name and password." });
}
}
catch (Exception ex)
{
return StatusCode(Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, ex.Message);
}
}

Related

How to manage client connections in a Blazor Server + ASP.NET Core API + SignalR project

I am working on a Blazor Server app over a SignalR connection with an ASP.NET Core API to send real-time updates from the server to clients. However, I am having a problem with managing user connections.
The uniqueness of the problem is that each tab opened in a browser by a user represents an individual connection on the SignalR server. This becomes a problem when a user has multiple tabs open with the application and each of them is generating a unique connection. This is because each tab is considered a unique session and therefore the SignalR server creates a new connection for each tab. For example:
If a user "User1" opens 3 tabs in their browser, 3 individual connections will be created for User1 on the server.
If another user "User2" opens 2 tabs in their browser, 2 more
connections will be created for User2 on the server.
And if I'm not logged in, and I open 3 tabs, it will create 3 more connections on
the server.
The desired environment, regardless of the number of tabs open, instead of duplicating connections:
User1 = 1 connection.
User2 = 1 connection.
Not logged = 1 connection.
My question is how can I effectively manage user connections so that there is only one connection per user/client/session instead of as many as opened tabs. Has anyone had a similar problem and knows how to solve it? I'm sorry if there is an usual easy know fix for this, but I'm looking around and I can't find something that fits exactly my behaviour; and I need some orientation on here instead of copy-paste some code, since conections managment it's a core feature and I'm not much familiar with these.
To clarifly some solutions I've tried are:
Sol. A) In the client: AddSingleton instead of AddScoped
Sol. B) In the client: Set the ConnectionId after hubConn.StartAsync()
Sol. B) In the server: Clients.Client(Context.ConnectionId).SendAsync() instead of Clients.All.SendAsync()
And to mention I didn't used services.AddHttpClient() w/
IClientFactory, but I dont know if it's needed at all or if it's involved in the problem.
Thank you for your time and help!
I provide code used in the connections below:
ASP.NET Core API - SERVER:
Program.cs
using ChartServer.DataProvider;
using ChartServer.RHub;
using SharedModels;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Add CORS Policy
builder.Services.AddCors(option => {
option.AddPolicy("cors", policy => {
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyHeader();
});
});
builder.Services.AddSignalR();
// Register the Watcher
builder.Services.AddScoped<TimeWatcher>();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseCors("cors");
app.UseAuthorization();
app.MapControllers();
// Add the SignalR Hub
app.MapHub<MarketHub>("/marketdata");
app.Run();
MarketController.cs
namespace ChartServer.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MarketController : ControllerBase
{
private IHubContext<MarketHub> marketHub;
private TimeWatcher watcher;
public MarketController(IHubContext<MarketHub> mktHub, TimeWatcher watch)
{
marketHub = mktHub;
watcher = watch;
}
[HttpGet]
public IActionResult Get()
{
if(!watcher.IsWatcherStarted)
{
watcher.Watcher(()=>marketHub.Clients.All.SendAsync("SendMarketStatusData",MarketDataProvider.GetMarketData()));
}
return Ok(new { Message = "Request Completed" });
}
}
}
MarketHub.cs
namespace ChartServer.RHub
{
public class MarketHub : Hub
{
public async Task AcceptData(List<Market> data) =>
await Clients.All.SendAsync("CommunicateMarketData", data);
}
}
TimeWatcher.cs
namespace ChartServer.DataProvider
{
/// <summary>
/// This call will be used to send the data after each second to the client
/// </summary>
public class TimeWatcher
{
private Action? Executor;
private Timer? timer;
// we need to auto-reset the event before the execution
private AutoResetEvent? autoResetEvent;
public DateTime WatcherStarted { get; set; }
public bool IsWatcherStarted { get; set; }
/// <summary>
/// Method for the Timer Watcher
/// This will be invoked when the Controller receives the request
/// </summary>
public void Watcher(Action execute)
{
int callBackDelayBeforeInvokeCallback = 1000;
int timeIntervalBetweenInvokeCallback = 2000;
Executor = execute;
autoResetEvent = new AutoResetEvent(false);
timer = new Timer((object? obj) => {
Executor();
}, autoResetEvent, callBackDelayBeforeInvokeCallback, timeIntervalBetweenInvokeCallback);
WatcherStarted = DateTime.Now;
IsWatcherStarted = true;
}
}
}
Blazor Server app - CLIENT:
Program.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using SignalsServer.Data;
using SignalsServer.HttpCaller;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://localhost:7084/") });
builder.Services.AddScoped<MarketDataCaller>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// 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.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
MarketDataCaller.cs
namespace SignalsServer.HttpCaller
{
public class MarketDataCaller
{
private HttpClient httpClient;
public MarketDataCaller(HttpClient http)
{
httpClient = http;
}
public async Task GetMarketDataAsync()
{
try
{
var response = await httpClient.GetAsync("marketdata");
if (!response.IsSuccessStatusCode)
throw new Exception("Something is wrong with the connection make sure that the server is running.");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw ex;
}
}
public async Task GetMarketEndpoint()
{
try
{
var response = await httpClient.GetAsync("https://localhost:7193/api/Market");
if (!response.IsSuccessStatusCode)
throw new Exception("Something is wrong with the connection so get call is not executing.");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw ex;
}
}
}
}
ChartComponent.razor
#page "/chartui"
#using Microsoft.AspNetCore.SignalR.Client;
#using SharedModels
#using System.Text.Json
#inject IJSRuntime js
#inject SignalsServer.HttpCaller.MarketDataCaller service;
<h3>Chart Component</h3>
<div>
<div class="container">
<table class="table table-bordered table-striped">
<tbody>
<tr>
<td>
<button class="btn btn-success"
#onclick="#generateLineChartTask">Line Chart</button>
</td>
<td>
<button class="btn btn-danger"
#onclick="#generateBarChartTask">Bar Chart</button>
</td>
</tr>
</tbody>
</table>
<div id="market"></div>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Company Name</th>
<th>Volume</th>
</tr>
</thead>
<tbody>
#foreach (var item in MarketData)
{
<tr>
<td>#item.CompanyName</td>
<td>#item.Volume</td>
</tr>
}
</tbody>
</table>
<hr/>
<div class="container">
#ConnectionStatusMessage
</div>
</div>
</div>
#code {
private HubConnection? hubConn;
private string? ConnectionStatusMessage;
public List<Market> MarketData = new List<Market>();
public List<Market> MarketReceivedData = new List<Market>();
private List<string> xSource;
private List<int> ySource;
private List<object> source;
protected override async Task OnInitializedAsync()
{
xSource = new List<string>();
ySource = new List<int>();
source = new List<object>();
await service.GetMarketEndpoint();
hubConn = new HubConnectionBuilder().WithUrl("https://localhost:7193/marketdata").Build();
await hubConn.StartAsync();
if(hubConn.State == HubConnectionState.Connected )
ConnectionStatusMessage = "Connection is established Successfully...";
else
ConnectionStatusMessage = "Connection is not established...";
}
private int contador = 0;
private void MarketDataListener(string chartType)
{
hubConn.On<List<Market>>("SendMarketStatusData", async (data) =>
{
MarketData = new List<Market>();
foreach (var item in data)
{
Console.WriteLine($"Company Name: {item.CompanyName}, Volumn: {item.Volume}");
xSource.Add(item.CompanyName);
ySource.Add(item.Volume);
}
source.Add(ySource);
source.Add(xSource);
MarketData = data;
contador++;
Console.WriteLine($"CONTADOR: {contador}");
InvokeAsync(StateHasChanged);
await js.InvokeAsync<object>(chartType, source.ToArray());
xSource.Clear();
ySource.Clear();
});
}
private void ReceivedMarketDataListener()
{
hubConn.On<List<Market>>("CommunicateMarketData", (data) =>
{
MarketReceivedData = data;
InvokeAsync(StateHasChanged);
});
}
public async Task Dispose()
{
await hubConn.DisposeAsync();
}
async Task generateLineChartTask()
{
MarketDataListener("marketLineChart");
ReceivedMarketDataListener();
await service.GetMarketDataAsync();
}
async Task generateBarChartTask()
{
MarketDataListener("marketBarChart");
ReceivedMarketDataListener();
await service.GetMarketDataAsync();
}
}
FULL CODE: https://github.com/maheshsabnis/SignalRChartBlazor
In the signalr application, opening the page in the browser will generate a new connectionId, which is the default behavior.
We can maintain the ConnectionIds of each user through the following sample code.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections.Features;
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace SignalRMiddleawre.Hubs
{
/// <summary>
/// </summary>
[Authorize]
public partial class MainHub : Hub
{
#region Connection
/// <summary>
/// Manage Connected Users
/// </summary>
private static ConcurrentDictionary<string?, List<string>>? ConnectedUsers = new ConcurrentDictionary<string?, List<string>>();
/// <summary>
/// OnConnect Event
/// </summary>
/// <param name="userid"></param>
/// <returns></returns>
///
public override async Task OnConnectedAsync()
{
// Get HttpContext In asp.net core signalr
//IHttpContextFeature hcf = (IHttpContextFeature)this.Context.Features[typeof(IHttpContextFeature)];
//HttpContext hc = hcf.HttpContext;
//string uid = hc.Request.Path.Value.Split(new string[] { "=", "" }, StringSplitOptions.RemoveEmptyEntries)[1].ToString();
string? userid = Context.User?.Identity?.Name;
if (userid == null || userid.Equals(string.Empty))
{
Trace.TraceInformation("user not loged in, can't connect signalr service");
return;
}
Trace.TraceInformation(userid + "connected");
// save connection
List<string>? existUserConnectionIds;
ConnectedUsers.TryGetValue(userid, out existUserConnectionIds);
if (existUserConnectionIds == null)
{
existUserConnectionIds = new List<string>();
}
existUserConnectionIds.Add(Context.ConnectionId);
ConnectedUsers.TryAdd(userid, existUserConnectionIds);
await Clients.All.SendAsync("ServerInfo", userid, userid + " connected, connectionId = " + Context.ConnectionId);
await base.OnConnectedAsync();
}
/// <summary>
/// OnDisconnected event
/// </summary>
/// <param name="userid"></param>
/// <returns></returns>
public override async Task OnDisconnectedAsync(Exception? exception)
{
string? userid = Context.User?.Identity?.Name;
// save connection
List<string>? existUserConnectionIds;
ConnectedUsers.TryGetValue(userid, out existUserConnectionIds);
existUserConnectionIds.Remove(Context.ConnectionId);
if (existUserConnectionIds.Count == 0)
{
List<string> garbage;
ConnectedUsers.TryRemove(userid, out garbage);
}
await base.OnDisconnectedAsync(exception);
}
#endregion
#region Message
/// <summary>
/// Send msg to all user
/// </summary>
/// <param name="userid"></param>
/// <param name="message"></param>
/// <returns></returns>
public async Task SendMessage(string msgType, string message)
{
await Clients.All.SendAsync("ReceiveMessage", msgType, message);
}
/// <summary>
/// Send msg to user by userid
/// </summary>
/// <param name="connectionId"></param>
/// <param name="message">message format : type-message </param>
/// <returns></returns>
public async Task SendToSingleUser(string userid, string message)
{
List<string>? existUserConnectionIds;
// find all the connectionids by userid
ConnectedUsers.TryGetValue(userid, out existUserConnectionIds);
if (existUserConnectionIds == null)
{
existUserConnectionIds = new List<string>();
}
existUserConnectionIds.Add(Context.ConnectionId);
ConnectedUsers.TryAdd(userid, existUserConnectionIds);
await Clients.Clients(existUserConnectionIds).SendAsync("ReceiveMessage", message);
}
#endregion
}
}

HttpPost with JSON parameter is not working in ASP.NET Core 3

So, I migrated my RestAPI project to ASP.NET Core 3.0 from ASP.NET Core 2.1 and the HttpPost function that previously worked stopped working.
[AllowAnonymous]
[HttpPost]
public IActionResult Login([FromBody]Application login)
{
_logger.LogInfo("Starting Login Process...");
IActionResult response = Unauthorized();
var user = AuthenticateUser(login);
if (user != null)
{
_logger.LogInfo("User is Authenticated");
var tokenString = GenerateJSONWebToken(user);
_logger.LogInfo("Adding token to cache");
AddToCache(login.AppName, tokenString);
response = Ok(new { token = tokenString });
_logger.LogInfo("Response received successfully");
}
return response;
}
Now, the login object has null values for each property. I read here, that
By default, when you call AddMvc() in Startup.cs, a JSON formatter, JsonInputFormatter, is automatically configured, but you can add additional formatters if you need to, for example to bind XML to an object.
Since AddMvc was removed in aspnetcore 3.0, now I feel this is why I am unable to get my JSON object anymore. My Startup class Configure function looks like this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseRouting();
//app.UseAuthorization();
//app.UseMvc(options
// /*routes => {
// routes.MapRoute("default", "{controller=Values}/{action}/{id?}");
//}*/);
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
}
The request I am sending through postman (raw and JSON options are selected)
{
"AppName":"XAMS",
"licenseKey": "XAMSLicenseKey"
}
UPDATES
Postman Header: Content-Type:application/json
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//_logger.LogInformation("Starting Log..."); //shows in output window
services.AddSingleton<ILoggerManager, LoggerManager>();
services.AddMemoryCache();
services.AddDbContext<GEContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
//services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddControllers();
services.AddRazorPages();
//Authentication
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "https://localhost:44387/";
options.Audience = "JWT:Issuer";
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);
options.RequireHttpsMetadata = false;
});
services.AddAuthorization(options =>
{
options.AddPolicy("GuidelineReader", p => {
p.RequireClaim("[url]", "GuidelineReader");
});
});
//
}
Application.cs
public class Application
{
public string AppName;
public string licenseKey;
}
With you updated code, I think the reason is you didn't create setter for your properties.
To fix the issue, change your Application model as below:
public class Application
{
public string AppName {get;set;}
public string licenseKey {get;set;}
}

IHttpActionResult return Json object

I have created one method in mvc api which returns string. But instead of returning string, I want to return Json Object. Here is my code.
[AllowAnonymous]
[HttpPost]
[Route("resetpassword")]
public IHttpActionResult ResetPassword(string email)
{
CreateUserAppService();
string newPassword =_userAppService.ResetPassword(email);
string subject = "Reset password";
string body = #"We have processed your request for password reset.<br/><br/>";
string from = ConfigurationManager.AppSettings[Common.Constants.FromEmailDisplayNameKey];
body = string.Format(body, newPassword, from);
SendEmail(email, subject, body, string.Empty);
return Ok<string>(newPassword);
}
Here it returns Ok<string>(newPassword); Now I want to return Json object. How can I return Json object?
Try that:
[AllowAnonymous]
[HttpPost]
[Route("resetpassword")]
public IHttpActionResult ResetPassword(string email)
{
//...
return Json(newPassword);
}
You are actually already using the key thing...
[HttpGet]
public IHttpActionResult Test()
{
return Ok(new {Password = "1234"});
}
You need to return it as CLR object so Web API serialize it to JSON, you can create your own POCO class or do it like this:
var passResponse = new
{
newPassword= yourNewPassword
};
But from security standpoint what you are doing is not correct, you should NEVER send plain passwords by email, you should reset user password by providing them a reset email link to your portal with some token and they should enter the new password. What you are doing here is not secure.
Create a return object.
public class PasswordResponse{
public string Password {get;set;}
//...other stuff to pass...
}
Then return an instance of the type in your response.
return OK(new PasswordResponse(){Password = newPassword});

Submitting JSON data to MVC controller. Why does it not handle complex types?

Define C# helper classes as:
`
public class SelectedFiles
{
public string Host { get; set; }
public List<SelectedFile> Files { get; set; }
}
public class SelectedFile
{
public string ShortName {get; set;}
public string Category { get; set; }
}`
And let define two controller methods (+ Deserialize helper):
private SelectedFiles Deserialize(string json)
{
using (MemoryStream stream = new MemoryStream(System.Text.ASCIIEncoding.ASCII.GetBytes(json)))
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(SelectedFiles));
return (SelectedFiles) serializer.ReadObject(stream);
}
}
/// <summary>
/// MVC seems to be incapable to deserizalize complex types.
/// </summary>
/// <param name="selectedFilesJSON"></param>
/// <returns></returns>
public void UploadFiles(SelectedFiles selectedFilesJSON)
{
var result = selectedFilesJSON;
}
/// <summary>
/// </summary>
/// <param name="selectedFilesJSON"></param>
/// <returns></returns>
public void UploadFilesJSON(string selectedFilesJSON)
{
if (!string.IsNullOrEmpty(selectedFilesJSON))
{
var result = this.Deserialize(selectedFilesJSON);
}
}
With javascript calling
var SelectedFiles = [];
var SelectedFile;
selectedCheckBoxes.each(function(idx, element){
SelectedFile = {ShortName: element.value, Category: "a category"};
SelectedFiles.push(SelectedFile);
});
var selectedFileMessage = {};
selectedFileMessage.Host = "test"
selectedFileMessage.Files = SelectedFiles;
var message = $.toJSON(selectedFileMessage);
$.ajax({url:"UploadFilesJSON", type:'POST', traditional:true, data: { selectedFilesJSON : message } , success: function(result){ alert(result);} });
$.ajax({url:"UploadFiles", type:'POST', traditional:true, data: { selectedFilesJSON : selectedFileMessage } , success: function(result){ alert(result);} });
The first .ajax POST method will work. Basically dumping the serialized JSON object as a string to the controller method.
The second .ajax POST does not work. It does not seems to figure out how to deserialize the 'SelectedFiles' helper class from the argument list.
One word: 'Flabergasted'
You have tagged your question with ASP.NET MVC 2, if that is correct you don't have JSON binding support out of the box. JsonValueProviderFactory is available since ASP.NET MVC 3. To bind JSON in previous versions of ASP.NET MVC you need to create a value provider or model binder. You can read more about it here: Sending JSON to an ASP.NET MVC Action Method Argument
In the second case the expected post data is the collection of SelectedFile. Have you tried
$.ajax({url:"UploadFiles", type:'POST', traditional:true, data: SelectedFiles, success: function(result){ alert(result);} });
You need to mark your POSTs contentType as json. Also, MVC will modelbind to your model so you don't need to use a string as an action parameter. Phil Haack has an example on his blog.

ASP.net MVC returning JSONP

I am looking to return some JSON across domains and I understand that the way to do this is through JSONP rather than pure JSON.
I am using ASP.net MVC so I was thinking about just extending the JsonResult type and then extending the Controller so that it also implemented a Jsonp method.
Is this the best way to go about it or is there a built-in ActionResult that might be better?
Solution: I went ahead and did that. Just for reference sake I added a new result:
public class JsonpResult : System.Web.Mvc.JsonResult
{
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
if (!String.IsNullOrEmpty(ContentType))
{
response.ContentType = ContentType;
}
else
{
response.ContentType = "application/javascript";
}
if (ContentEncoding != null)
{
response.ContentEncoding = ContentEncoding;
}
if (Data != null)
{
// The JavaScriptSerializer type was marked as obsolete prior to .NET Framework 3.5 SP1
#pragma warning disable 0618
HttpRequestBase request = context.HttpContext.Request;
JavaScriptSerializer serializer = new JavaScriptSerializer();
response.Write(request.Params["jsoncallback"] + "(" + serializer.Serialize(Data) + ")");
#pragma warning restore 0618
}
}
}
and also a couple of methods to a superclass of all my controllers:
protected internal JsonpResult Jsonp(object data)
{
return Jsonp(data, null /* contentType */);
}
protected internal JsonpResult Jsonp(object data, string contentType)
{
return Jsonp(data, contentType, null);
}
protected internal virtual JsonpResult Jsonp(object data, string contentType, Encoding contentEncoding)
{
return new JsonpResult
{
Data = data,
ContentType = contentType,
ContentEncoding = contentEncoding
};
}
Works like a charm.
Here is a simple solution, if you don't want to define an action filter
Client side code using jQuery:
$.ajax("http://www.myserver.com/Home/JsonpCall", { dataType: "jsonp" }).done(function (result) {});
MVC controller action. Returns content result with JavaScript code executing callback function provided with query string. Also sets JavaScript MIME type for response.
public ContentResult JsonpCall(string callback)
{
return Content(String.Format("{0}({1});",
callback,
new JavaScriptSerializer().Serialize(new { a = 1 })),
"application/javascript");
}
Rather than subclassing my controllers with Jsonp() methods, I went the extension method route as it feels a touch cleaner to me. The nice thing about the JsonpResult is that you can test it exactly the same way you would a JsonResult.
I did:
public static class JsonResultExtensions
{
public static JsonpResult ToJsonp(this JsonResult json)
{
return new JsonpResult { ContentEncoding = json.ContentEncoding, ContentType = json.ContentType, Data = json.Data, JsonRequestBehavior = json.JsonRequestBehavior};
}
}
This way you don't have to worry about creating all the different Jsonp() overloads, just convert your JsonResult to a Jsonp one.
Ranju's blog post (aka "This blog post I found") is excellent, and reading it will allow you to further the solution below so that your controller can handle same-domain JSON and cross-domain JSONP requests elegantly in the same controller action without additional code [in the action].
Regardless, for the "give me the code" types, here it is, in case the blog disappears again.
In your controller (this snippet is new/non-blog code):
[AllowCrossSiteJson]
public ActionResult JsonpTime(string callback)
{
string msg = DateTime.UtcNow.ToString("o");
return new JsonpResult
{
Data = (new
{
time = msg
})
};
}
JsonpResult found on
this excellent blog post:
/// <summary>
/// Renders result as JSON and also wraps the JSON in a call
/// to the callback function specified in "JsonpResult.Callback".
/// http://blogorama.nerdworks.in/entry-EnablingJSONPcallsonASPNETMVC.aspx
/// </summary>
public class JsonpResult : JsonResult
{
/// <summary>
/// Gets or sets the javascript callback function that is
/// to be invoked in the resulting script output.
/// </summary>
/// <value>The callback function name.</value>
public string Callback { get; set; }
/// <summary>
/// Enables processing of the result of an action method by a
/// custom type that inherits from <see cref="T:System.Web.Mvc.ActionResult"/>.
/// </summary>
/// <param name="context">The context within which the
/// result is executed.</param>
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
HttpResponseBase response = context.HttpContext.Response;
if (!String.IsNullOrEmpty(ContentType))
response.ContentType = ContentType;
else
response.ContentType = "application/javascript";
if (ContentEncoding != null)
response.ContentEncoding = ContentEncoding;
if (Callback == null || Callback.Length == 0)
Callback = context.HttpContext.Request.QueryString["callback"];
if (Data != null)
{
// The JavaScriptSerializer type was marked as obsolete
// prior to .NET Framework 3.5 SP1
#pragma warning disable 0618
JavaScriptSerializer serializer = new JavaScriptSerializer();
string ser = serializer.Serialize(Data);
response.Write(Callback + "(" + ser + ");");
#pragma warning restore 0618
}
}
}
Note: Following up on the comments to the OP by #Ranju and others, I figured it was worth posting the "bare minimum" functional code from Ranju's blog post as a community wiki. Though it's safe to say that Ranju added the above and other code on his blog to be used freely, I'm not going to copy his words here.
For ASP.NET Core ,NOT ASP.NET MVC
This is a tailored version for ASP.NET CORE of the solution which exists in the answer
public class JsonpResult : JsonResult
{
public JsonpResult(object value) : base(value)
{
}
public override async Task ExecuteResultAsync(ActionContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
HttpResponse response = context.HttpContext.Response;
if (!String.IsNullOrEmpty(ContentType))
response.ContentType = ContentType;
else
response.ContentType = "application/javascript";
if (Value != null)
{
HttpRequest request = context.HttpContext.Request;
string serializedJson = JsonConvert.SerializeObject(Value);
string result = $"{request.Query["callback"]}({serializedJson})";
await response.WriteAsync(result);
}
}
}
The referenced articles by stimms and ranju v were both very useful and made the situation clear.
However, I was left scratching my head about using extensions, sub-classing in context of the MVC code I had found online.
There was two key points that caught me out:
The code I had derived from ActionResult, but in ExecuteResult there was some code to return either XML or JSON.
I had then created a Generics based ActionResult, to ensure the same ExecuteResults was used independant of the type of data I returned.
So, combining the two - I did not need further extensions or sub-classing to add the mechanism to return JSONP, simply change my existing ExecuteResults.
What had confused me is that really I was looking for a way to derive or extend JsonResult, without re-coding the ExecuteResult. As JSONP is effectively a JSON string with prefix & suffix it seemed a waste. However the underling ExecuteResult uses respone.write - so the safest way of changing is to re-code ExecuteResults as handily provided by various postings!
I can post some code if that would be useful, but there is quite a lot of code in this thread already.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Script.Serialization;
namespace Template.Web.Helpers
{
public class JsonpResult : JsonResult
{
public JsonpResult(string callbackName)
{
CallbackName = callbackName;
}
public JsonpResult()
: this("jsoncallback")
{
}
public string CallbackName { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
string jsoncallback = ((context.RouteData.Values[CallbackName] as string) ?? request[CallbackName]) ?? CallbackName;
if (!string.IsNullOrEmpty(jsoncallback))
{
if (string.IsNullOrEmpty(base.ContentType))
{
base.ContentType = "application/x-javascript";
}
response.Write(string.Format("{0}(", jsoncallback));
}
base.ExecuteResult(context);
if (!string.IsNullOrEmpty(jsoncallback))
{
response.Write(")");
}
}
}
public static class ControllerExtensions
{
public static JsonpResult Jsonp(this Controller controller, object data, string callbackName = "callback")
{
return new JsonpResult(callbackName)
{
Data = data,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
public static T DeserializeObject<T>(this Controller controller, string key) where T : class
{
var value = controller.HttpContext.Request.QueryString.Get(key);
if (string.IsNullOrEmpty(value))
{
return null;
}
JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();
return javaScriptSerializer.Deserialize<T>(value);
}
}
}
//Example of using the Jsonp function::
// 1-
public JsonResult Read()
{
IEnumerable<User> result = context.All();
return this.Jsonp(result);
}
//2-
public JsonResult Update()
{
var models = this.DeserializeObject<IEnumerable<User>>("models");
if (models != null)
{
Update(models); //Update properties & save change in database
}
return this.Jsonp(models);
}
the solution above is a good way of working but it should be extendend with a new type of result instead of having a method that returns a JsonResult you should write methods that return your own result types
public JsonPResult testMethod() {
// use the other guys code to write a method that returns something
}
public class JsonPResult : JsonResult
{
public FileUploadJsonResult(JsonResult data) {
this.Data = data;
}
public override void ExecuteResult(ControllerContext context)
{
this.ContentType = "text/html";
context.HttpContext.Response.Write("<textarea>");
base.ExecuteResult(context);
context.HttpContext.Response.Write("</textarea>");
}
}