How to send object which contains IEnumerable via Refit on NetCore? - asp.net-core

I have to send a request object via Refit which contains 2 IEnumerable and one string, but for some reason I can't send the object forward.
I've tried to use all the paramets from the interface. Ex: [Query(CollectionFormat.Csv)] or Multi / Pipes but no success.
I've also tried to create my own CustomUrlParameterFormatter but unfortunately here I'm stuck, because I don't see a good way to retrieve the name of the property from the object request that I'm sending.
The code for CustomUrlParameterFormatter
public class CustomUrlParameterFormatter : IUrlParameterFormatter
{
public string Format(object value, ParameterInfo parameterInfo)
{
if(value is IEnumerable enumerable)
{
var result = ToQueryString(enumerable, parameterInfo.Name);
return result;
}
return string.Empty;
}
public static string ToQueryString(IEnumerable query, string parameterName)
{
var values = query.Cast<object>().Select(ToString).ToArray();
var separator = parameterName + "=";
return values.Any() ? separator + string.Join("&" + separator, values) : "";
}
public static string ToString(object value)
{
var json = JsonConvert.SerializeObject(value).Replace("\\\"", "\"").Trim('"');
return Uri.EscapeUriString(json);
}
}
The Call from the IService that I'm using
[Get("/TestMethod")]
Task<HttpResponseMessage> TestMethod([Query]TestRequestDTO requestDTO, [Header("X-Correlation-ID")] string correlationId);
The Request object
public class TestRequestDTO
{
public IEnumerable<long> EnumOne { get; set; }
public IEnumerable<long> EnumTwo { get; set; }
public string MethodString { get; set; }
}
Also the RefitClient configuration
var refitSettings = new RefitSettings();
refitSettings.UrlParameterFormatter = new CustomUrlParameterFormatter();
services.AddRefitClient<IService>(refitSettings)
.ConfigureHttpClient(c => c.BaseAddress = new Uri(settings.Services.IService));
What I'm trying to achieve is something like
TestMethod?EnumOne =123&EnumOne =321&EnumTwo=123&EnumTwo=321&methodString=asdsaa
and instead I'm receiving other behavior
without CustomUrlParameterFormatter()
TestMethod?EnumOne=System.Collections.Generic.List`1%5BSystem.Int64%5D&EnumTwo=System.Collections.Generic.List`1%5BSystem.Int64%5D&MethodString=sdf

Related

.NET CORE WEB API accept list of integers as an input param in HTTP GET API

I am using .net core 3+ web api.
Below is how my action looks like below, it uses HTTP GET and I want to pass few fields and one of the fields is a list of integers.
[HttpGet]
[Route("cities")]
public ActionResult<IEnumerable<City>> GetCities([FromQuery] CityQuery query)
{...}
and here is CityQuery class -
public class CityQuery
{
[FromQuery(Name = "stateids")]
[Required(ErrorMessage = "stateid is missing")]
public string StateIdsStr { get; set; }
public IEnumerable<int> StateList
{
get
{
if (!string.IsNullOrEmpty(StateIdsStr))
{
var output = StateIdsStr.Split(',').Select(id =>
{
int.TryParse(id, out var stateId);
return stateId;
}).ToList();
return output;
}
return new List<int>();
}
}
}
Is there a generic way I can use to accept list of integers as input and not accept string and then parse it?
Or is there a better way to do this? I tried googling but could not find much. Thanks in advance.
This can help
[HttpGet]
[Route("cities")]
public ActionResult<IEnumerable<City>> GetCities([FromQuery] int[] stateids)
{
...
}
but the query string will change to
https://localhost/api/controller/cities?stateids=1&stateids=2&stateids=3
If you required comma separated query string with integer, you can go for Custom model binder
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-3.1
You can use custom model binding, below is a working demo:
Model:
public class CityQuery
{
public List<int> StateList{ get; set; }
}
CustomModelBinder:
public class CustomModelBinder: IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var values = bindingContext.ValueProvider.GetValue("stateids");
if (values.Length == 0)
{
return Task.CompletedTask;
}
var splitData = values.FirstValue.Split(',');
var result = new CityQuery()
{
StateList = new List<int>()
};
foreach(var id in splitData)
{
result.StateList.Add(int.Parse(id));
}
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
Applying ModelBinding Attribute on Action method:
[HttpGet]
[Route("cities")]
public ActionResult GetCities([ModelBinder(BinderType = typeof(CustomModelBinder))] CityQuery query)
{
return View();
}
when the url like /cities?stateids=1,2,3, the stateids will be filled to StateList
I think you just need to use [FromUri] before int array parameter :
public ActionResult<IEnumerable<City>> GetCities([FromUri] int[] stateList)
And request would be like :
/cities?stateList=1&stateList=2&stateList=3

How to send complex data to controller endpoint

I have this basic case:
[HttpPost("endpoint")]
public IActionResult Endpoint(DateTime date, string value, bool modifier)
{
return Ok($"{date}-{value}-{modifier}");
}
and I'm able to send a request to it with
var testContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "date", DateTime.Today.ToShortDateString() },
{ "value", "value1" },
{ "modifier", true.ToString() }
});
Instead I want my endpoint to be this instead
[HttpPost("endpointwithlist")]
public IActionResult EndpointWithList(DateTime date, List<string> value, bool modifier)
{
return Ok($"{date}-{value.FirstOrDefault()}-{modifier}");
}
How do I send this? I have tried the below, nothing works
var json = JsonConvert.SerializeObject(new { date, value = valueCollection.ToArray(), modifier });
var testContentWithList = new ByteArrayContent(Encoding.UTF8.GetBytes(json));
testContentWithList.Headers.ContentType = new MediaTypeHeaderValue("application/json");
You might create a model class for the payload
public class EndpointWithListModel
{
public DateTime Date {get; set;}
public List<string> Value {get; set;}
public bool Modifier {get; set;}
}
the method parameter then could use [FromBody] attribute
public IActionResult EndpointWithList([FromBody]EndpointWithListModel model)
then send the json to your POST method, example is here. Using HttpClient:
using (var client = new HttpClient())
{
var response = await client.PostAsync(
"http://yourUrl",
new StringContent(json, Encoding.UTF8, "application/json"));
}
if your variables(date, valueController and modifier) are in the right type, following code should work.
var json = JsonConvert.SerializeObject(new { date:date, value : valueCollection.ToArray(), modifier:modifier });

.Net Core - Using Authorization filter to check a variable from request and modify it

In below code, I need to read value of variable from request and modify request object.
public class ApiAuthFilter : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
// 1. read request object from context
// 2. modify the value
// 3. add the value and update request
}
}
You could modify request object like below:
1.User:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
2.ApiAuthFilter:
public class ApiAuthFilter : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
//1.read request object from context
var request = context.HttpContext.Request;
request.EnableRewind();
request.Body.Position = 0;
using (var reader = new StreamReader(request.Body))
{
//2.modify the value
var decriptedFromJavascript = "{ \"Id\":2,\"Name\":\"UR123456\"}";
byte[] bytes = Encoding.ASCII.GetBytes(decriptedFromJavascript);
//3. add the value and update request
request.Body = new MemoryStream(bytes);
}
}
}
3.Action:
[HttpPost("[action]")]
[ApiAuthFilter]
public User GetUser([FromBody]User user)
{
return user;
}
4.Result:
public void OnAuthorization(AuthorizationFilterContext context)
{
string authHeader = context.HttpContext.Request.Headers["Authorization"];
context.HttpContext.Request.Headers["Authorization"] = "value";
}

Change injected object at runtime

I want to have multiples implementation of the IUserRepository each implementation will work with a database type either MongoDB or any SQL database. To do this I have ITenant interface that have a connection string and other tenant configuration. The tenant is been injected into IUserRepository either MongoDB or any SQLDB implementation. What I need to know is how properly change the injected repository to choose the database base on the tenant.
Interfaces
public interface IUserRepository
{
string Login(string username, string password);
string Logoff(Guid id);
}
public class User
{
public Guid Id { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
public interface ITenant
{
string CompanyName { get; }
string ConnectionString { get; }
string DataBaseName { get; }
string EncriptionKey { get; }
}
Is important to know that the tenant id is been pass to an API via header request
StartUp.cs
// set inject httpcontet to the tenant implemantion
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();
// inject tenant
services.AddTransient<ITenant, Tenant>();
// inject mongo repository but I want this to be programmatically
services.AddTransient<IUserRepository, UserMongoRepository>();
Sample Mongo Implementation
public class UserMongoRepository : IUserRepository
{
protected ITenant Tenant
public UserMongoRepository(ITenant tenant) :
base(tenant)
{
this.Tenant = tenant;
}
public string Login(string username, string password)
{
var query = new QueryBuilder<User>().Where(x => x.Username == username);
var client = new MongoClient(this.Tenant.ConnectionString);var server = client.GetServer();
var database = client.GetServer().GetDatabase(this.Tenant.DataBaseName);
var user = database.GetCollection<User>.FindAs<User>(query).AsQueryable().FirstOrDefault();
if (user == null)
throw new Exception("invalid username or password");
if (user.Password != password)
throw new Exception("invalid username or password");
return "Sample Token";
}
public string Logoff(Guid id)
{
throw new NotImplementedException();
}
}
Tenant
public class Tenant : ITenant
{
protected IHttpContextAccessor Accesor;
protected IConfiguration Configuration;
public Tenant(IHttpContextAccessor accesor, IDBConfiguration config)
{
this.Accesor = accesor;
this.Configuration = new Configuration().AddEnvironmentVariables();
if (!config.IsConfigure)
config.ConfigureDataBase();
}
private string _CompanyName;
public string CompanyName
{
get
{
if (string.IsNullOrWhiteSpace(_CompanyName))
{
_CompanyName = this.Accesor.Value.Request.Headers["Company"];
if (string.IsNullOrWhiteSpace(_CompanyName))
throw new Exception("Invalid Company");
}
return _CompanyName;
}
}
private string _ConnectionString;
public string ConnectionString
{
get
{
if (string.IsNullOrWhiteSpace(_ConnectionString))
{
_ConnectionString = this.Configuration.Get(this.CompanyName + "_" + "ConnectionString");
if (string.IsNullOrWhiteSpace(_ConnectionString))
throw new Exception("Invalid ConnectionString Setup");
}
return _ConnectionString;
}
}
private string _EncriptionKey;
public string EncriptionKey
{
get
{
if (string.IsNullOrWhiteSpace(_EncriptionKey))
{
_EncriptionKey = this.Configuration.Get(this.CompanyName + "_" + "EncriptionKey");
if (string.IsNullOrWhiteSpace(_EncriptionKey))
throw new Exception("Invalid Company Setup");
}
return _EncriptionKey;
}
}
private string _DataBaseName;
public string DataBaseName
{
get
{
if (string.IsNullOrWhiteSpace(_DataBaseName))
{
_DataBaseName = this.Configuration.Get(this.CompanyName + "_" + "DataBaseName");
if (string.IsNullOrWhiteSpace(_DataBaseName))
throw new Exception("Invalid Company Setup");
}
return _DataBaseName;
}
}
}
Controller
public class UsersController : Controller
{
protected IUserRepository DataService;
public UsersController(IUserRepository dataService)
{
this.DataService = dataService;
}
// the controller implematation
}
You should define a proxy implementation for IUserRepository and hide the actual implementations behind this proxy and at runtime decide which repository to forward the call to. For instance:
public class UserRepositoryDispatcher : IUserRepository
{
private readonly Func<bool> selector;
private readonly IUserRepository trueRepository;
private readonly IUserRepository falseRepository;
public UserRepositoryDispatcher(Func<bool> selector,
IUserRepository trueRepository, IUserRepository falseRepository) {
this.selector = selector;
this.trueRepository = trueRepository;
this.falseRepository = falseRepository;
}
public string Login(string username, string password) {
return this.CurrentRepository.Login(username, password);
}
public string Logoff(Guid id) {
return this.CurrentRepository.Logoff(id);
}
private IRepository CurrentRepository {
get { return selector() ? this.trueRepository : this.falseRepository;
}
}
Using this proxy class you can easily create a runtime predicate that decides which repository to use. For instance:
services.AddTransient<IUserRepository>(c =>
new UserRepositoryDispatcher(
() => c.GetRequiredService<ITenant>().DataBaseName.Contains("Mongo"),
trueRepository: c.GetRequiredService<UserMongoRepository>()
falseRepository: c.GetRequiredService<UserSqlRepository>()));
You can try injecting a factory rather than the actual repository. The factory will be responsible for building the correct repository based on the current user identity.
It might require a little more boiler plate code but it can achieve what you want. A little bit of inheritance might even make the controller code simpler.

How to get WebAPI XML serializer to use TypeConverter

In a ASP.NET Web API project, I've got a custom type with a TypeConverter that handles the to and from for string conversions, as described here:
http://blogs.msdn.com/b/jmstall/archive/2012/04/20/how-to-bind-to-custom-objects-in-action-signatures-in-mvc-webapi.aspx
The custom type is used as a property in my model and this works great. The json for my model object includes my custom object as the string value created from my TypeConverter.
So then I switch the request to accept XML instead and the TypeConverter is no longer invoked and my object is incorrectly serialized.
How to I get the XML serializer to utilize the TypeConverter as the JSON one does. The article referenced made it sound like the use of TypeConverters, when present, was a predictable feature. If it is completely at the whim of the serializer then making APIs that express data in both XML and JSON in a consistent way is nearly impossible.
Sample code from the article, plus a bit of context:
[TypeConverter(typeof(LocationTypeConverter))]
public class Location
{
public int X { get; set; }
public int Y { get; set; }
// Parse a string into a Location object. "1,2" --> Loc(X=1,Y=2)
public static Location TryParse(string input)
{
var parts = input.Split(',');
if (parts.Length != 2)
{
return null;
}
int x,y;
if (int.TryParse(parts[0], out x) && int.TryParse(parts[1], out y))
{
return new Location { X = x, Y = y };
}
return null;
}
public override string ToString()
{
return string.Format("{0},{1}", X, Y);
}
}
public class LocationTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
return Location.TryParse((string) value);
}
return base.ConvertFrom(context, culture, value);
}
}
Create a model with that class:
public class SampleModel
{
public int One { get; set;}
public string Two { get; set;}
public Location Three { get; set;}
}
Create a ApiController with a method like this:
public SampleModel Get()
{
return new SapleModel { One = 1, Two = "Two", Three = new Location { X = 111, Y = 222 } };
}
That's it. Using fiddler and try them both and note that Location serializes using the converter into the single-line comma-seperated format like this (Three="111,222") with Json but not with XML.