Shopifysharp AuthorizationService.IsAuthenticRequest Returns false - shopify

Please see the following code snippet:
This is my controller:
[Produces("application/json")]
[Route("api/Shopify")]
[AllowAnonymous]
[ServiceFilter(typeof(ShopifyVerificationFilter))]
//[ApiExplorerSettings(IgnoreApi = true)]
public class ShopifyController : Controller
{
private readonly ILogger logger;
public ShopifyController(ILoggerFactory loggerFactory)
{
logger = loggerFactory.CreateLogger<StoreLocatorController>();
}
[HttpGet("fetch_stock.json")]
public async Task<IActionResult> GetInventoryLevels(ShopifyFetchStock shopifyFetchStock, [FromServices] IShopifyFulfillmentServices shopifyFulfillmentServices)
{
try
{
var inventoryData = await shopifyFulfillmentServices.GetInventoryLevels(shopifyFetchStock);
return Ok(inventoryData);
}
catch (Exception ex)
{
return Ok();
}
}
}
This is my ShopifyVerificationFilter:
public class ShopifyVerificationFilter : Attribute, IAuthorizationFilter
{
private readonly IOptions<ShopifySettings> _shopifySettings;
private readonly IShopifyVerify _shopifyVerify;
public ShopifyVerificationFilter(IOptions<ShopifySettings> shopifySettings, IShopifyVerify shopifyVerify)
{
_shopifySettings = shopifySettings;
_shopifyVerify = shopifyVerify;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var isVerified = _shopifyVerify.IsAuthenticShopifyRequest(context.HttpContext.Request.QueryString.Value, _shopifySettings.Value.APISecretKey);
if (!isVerified)
{
context.HttpContext.Request.EnableRewind();
isVerified = _shopifyVerify.IsAuthenticShopifyWebhook(context.HttpContext.Request.Headers, context.HttpContext.Request.Body, _shopifySettings.Value.APISecretKey, context.HttpContext.Request.QueryString.Value);
if (!isVerified)
{
context.Result = new UnauthorizedResult();
}
else
{
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
}
}
}
}
This is the implementation for the IsAuthenticShopifyRequest method:
public class ShopifyVerify : IShopifyVerify
{
public bool IsAuthenticShopifyRequest(string queryString, string APIKey)
{
var result = AuthorizationService.IsAuthenticRequest(queryString, APIKey);
return result;
}
}
When a call is made to AuthorizationService.IsAuthenticShopifyRequest(string queryString, string APIKey), it always returns false and thus not able to authenticate the shop. This piece of code was running without error before now. This issue started some couple of weeks back.
Did anything change in shopifysharp? If not please what do I need to do to get this work and if shopifysharp changed the implementation of AuthorizationService.IsAuthenticRequest(queryString, APIKey); please I need help in resolving this.
Thanks.

Related

Read ASP.NET Core logs per scope/operation

Let's say I have several ASP.NET BackgroundServices and each is logging to its own scope/operation (OP1 and OP2).
public class MyBackgroundService1 : BackgroundService
{
private readonly ILogger<MyBackgroundService1> _logger;
public MyBackgroundService1(ILogger<MyBackgroundService1> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var activity = new Activity("OP1");
activity.Start();
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Hello from MyBackgroundService1");
await Task.Delay(5000, stoppingToken);
}
}
}
public class MyBackgroundService2 : BackgroundService
{
private readonly ILogger<MyBackgroundService2> _logger;
public MyBackgroundService2(ILogger<MyBackgroundService2> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var activity = new Activity("OP2");
activity.Start();
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Hello from MyBackgroundService2");
await Task.Delay(1000, stoppingToken);
}
}
}
Now I would like to use Blazor and want to display a table per operation with all corresponding logs.
Example output
OP1 Logs:
Hello from MyBackgroundService1
Hello from MyBackgroundService1
OP2 Logs:
Hello from MyBackgroundService2
Hello from MyBackgroundService2
How would I do that?
For this purpose, you need to create a log provider that stores the information in the database and then retrieves the information from the log table.
First, create a class to store logs in the database as follows:
public class DBLog
{
public int DBLogId { get; set; }
public string? LogLevel { get; set; }
public string? EventName { get; set; }
public string? Message { get; set; }
public string? StackTrace { get; set; }
public DateTime CreatedDate { get; set; }=DateTime.Now;
}
Now, We need to create a custom DBLogger. The DBLogger class inherits from the ILogger interface and has three methods, the most important of which is the Log method, which is actually called every time the Logger is called in the program. To read more about the other two methods, you can refer here.
public class DBLogger:ILogger
{
private readonly LogLevel _minLevel;
private readonly DbLoggerProvider _loggerProvider;
private readonly string _categoryName;
public DBLogger(
DbLoggerProvider loggerProvider,
string categoryName
)
{
_loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider));
_categoryName= categoryName;
}
public IDisposable BeginScope<TState>(TState state)
{
return new NoopDisposable();
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= _minLevel;
}
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
var message = formatter(state, exception);
if (exception != null)
{
message = $"{message}{Environment.NewLine}{exception}";
}
if (string.IsNullOrEmpty(message))
{
return;
}
var dblLogItem = new DBLog()
{
EventName = eventId.Name,
LogLevel = logLevel.ToString(),
Message = $"{_categoryName}{Environment.NewLine}{message}",
StackTrace=exception?.StackTrace
};
_loggerProvider.AddLogItem(dblLogItem);
}
private class NoopDisposable : IDisposable
{
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
}
}
}
Now we need to create a custom log provider so that an instance of the above custom database logger (DBLogger) can be created.
public class DbLoggerProvider : ILoggerProvider
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly IList<DBLog> _currentBatch = new List<DBLog>();
private readonly TimeSpan _interval = TimeSpan.FromSeconds(2);
private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>());
private readonly Task _outputTask;
private readonly IServiceProvider _serviceProvider;
private bool _isDisposed;
public DbLoggerProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_outputTask = Task.Run(ProcessLogQueue);
}
public ILogger CreateLogger(string categoryName)
{
return new DBLogger(this, categoryName);
}
private async Task ProcessLogQueue()
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
while (_messageQueue.TryTake(out var message))
{
try
{
_currentBatch.Add(message);
}
catch
{
//cancellation token canceled or CompleteAdding called
}
}
await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token);
_currentBatch.Clear();
await Task.Delay(_interval, _cancellationTokenSource.Token);
}
}
internal void AddLogItem(DBLog appLogItem)
{
if (!_messageQueue.IsAddingCompleted)
{
_messageQueue.Add(appLogItem, _cancellationTokenSource.Token);
}
}
private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken)
{
try
{
if (!items.Any())
{
return;
}
// We need a separate context for the logger to call its SaveChanges several times,
// without using the current request's context and changing its internal state.
var scopeFactory = _serviceProvider.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
var scopedProvider = scope.ServiceProvider;
using (var newDbContext = scopedProvider.GetRequiredService<ApplicationDbContext>())
{
foreach (var item in items)
{
var addedEntry = newDbContext.DbLogs.Add(item);
}
await newDbContext.SaveChangesAsync(cancellationToken);
// ...
}
}
}
catch
{
// don't throw exceptions from logger
}
}
[SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception",
Justification = "don't throw exceptions from logger")]
private void Stop()
{
_cancellationTokenSource.Cancel();
_messageQueue.CompleteAdding();
try
{
_outputTask.Wait(_interval);
}
catch
{
// don't throw exceptions from logger
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
try
{
if (disposing)
{
Stop();
_messageQueue.Dispose();
_cancellationTokenSource.Dispose();
}
}
finally
{
_isDisposed = true;
}
}
}
}
In the end, it is enough to call this custom log provider (DbLoggerProvider) in the Startup.cs or Program.cs class.
var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
loggerFactory.AddProvider(new DbLoggerProvider(serviceProvider));
From now on, every time we call the _logger.LogInformation("");, the log information will also be stored in the database.
Note: Because the number of calls to record logs in the database may be high, a concurrent queue is used to store logs.
If you like, you can refer to my repository that implements the same method.
In order to log the areas separately(scope/operation), you can create several different DBLoggers to store the information in different tables.

Admin lock or unlock account user in .Net Core

I am doing the management of a user's account when necessary I can Lock a user's account in case they violate it. Or can be unlocked if required. I got an error like this. Where am I wrong, I use .Net Core 5 to build my program. Error: "An unhandled exception occurred while processing the request.
NullReferenceException: Object reference not set to an instance of an object."
enter image description here
Interface
public bool LockUser(string email);
public bool UnlockUser(string email);
Repo
public bool LockUser(string email)
{
var userTask = _userManager.FindByEmailAsync(email);
userTask.Wait();
var user = userTask.Result;
var lockUserTask = _userManager.SetLockoutEnabledAsync(user, true);
lockUserTask.Wait();
var lockDateTask = _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now);
lockDateTask.Wait();
return lockDateTask.Result.Succeeded && lockUserTask.Result.Succeeded;
}
Controller
public ActionResult LockUser(string email)
{
if (!_userRepository.LockUser(email))
{
throw new ArgumentException("Error");
}
return RedirectToAction("Index");
}
Please refer the following sample code, the UserRepository should like this, add the usermanager via the constructor parameter:
public interface IUserRepository
{
public bool LockUser(string email);
public bool UnlockUser(string email);
}
public class UserRepository : IUserRepository
{
private readonly UserManager<IdentityUser> _userManager;
public UserRepository(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
public bool LockUser(string email)
{
var userTask = _userManager.FindByEmailAsync(email);
userTask.Wait();
var user = userTask.Result;
var lockUserTask = _userManager.SetLockoutEnabledAsync(user, true);
lockUserTask.Wait();
var lockDateTask = _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now);
lockDateTask.Wait();
return lockDateTask.Result.Succeeded && lockUserTask.Result.Succeeded;
}
public bool UnlockUser(string email)
{
//...
throw new NotImplementedException();
}
}
Then, add the service to the service container:
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddScoped<IUserRepository, UserRepository>();
services.AddControllersWithViews();
}
Then, in the MVC controller:
public class HomeController : Controller
{
private readonly IUserRepository _userRepository;
public HomeController(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public IActionResult Index(int id)
{
string email = "aa#hotmail.com";
if (!_userRepository.LockUser(email))
{
throw new ArgumentException("Error");
}
return View();
}
The debug screenshot like this:

.NET Core 3 Service result to Controller to FE (data or error reason)

building new app on .net core 3 and Angular. Overall all works, but I want to add more intelligence to service/controller part. This is one of the api's, but this logic can be applied to others as as well.
Here's my Login Controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLogin)
{
var token = await _userService.LoginAsync(userLogin);
if (token != null)
{
return Ok(token);
}
else
{
return BadRequest("Something went wrong");
}
}
And here's my userService:
public async Task<string> LoginAsync(UserLoginDto userLogin)
{
var user = await _userManager.FindByEmailAsync(userLogin.Email);
if (user != null)
{
var result = await _signInManager.PasswordSignInAsync(user, userLogin.Password, false, true);
if (result.Succeeded)
{
var roles = await _userManager.GetRolesAsync(user);
var tokenJson = _jwtManager.getJwtToken(user.Email, roles);
return tokenJson;
}
else
{
return null; // Return BadRequest and result reason (Failed, lockedout, etc)
}
}
else
{
return null; // User not found, return NotFound }
}
Here's my question - how should I return result from userService to Controller so, that I could respond to API call either with Ok(token) or BadRequest/NotFound with the reason.
If I keep all this LoginAsync code in controller, then it's easy, but I want to use service.
One option I was thinking was to introduce new class, something like:
public class BaseResult
{
public object Data { get; set; }
public long ResponseCode { get; set; }
public string ErrorMessage { get; set; }
}
then always return this class from service, but not fully like that idea either.
thanks!
Here is a working demo you could follow:
Model:
public class UserLoginDto
{
public string Email { get; set; }
public string Password { get; set; }
}
IUserService:
public interface IUserService
{
Task<IActionResult> LoginAsync(UserLoginDto userLogin);
}
UserService:
public class UserService: IUserService
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IJwtManager _jwtManager;
public UserService(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
IJwtManager jwtManager)
{
_userManager = userManager;
_signInManager = signInManager;
_jwtManager = jwtManager;
}
public async Task<IActionResult> LoginAsync(UserLoginDto userLogin)
{
var user = await _userManager.FindByEmailAsync(userLogin.Email);
if (user != null)
{
var result = await _signInManager.PasswordSignInAsync(user, userLogin.Password, false, true);
if (result.Succeeded)
{
var roles = await _userManager.GetRolesAsync(user);
var tokenJson = _jwtManager.getJwtToken(user.Email, roles);
return new OkObjectResult(tokenJson);
}
else
{
// Return BadRequest and result reason (Failed, lockedout, etc)
if (result.IsNotAllowed)
{
if (!await _userManager.IsEmailConfirmedAsync(user))
{
// Email isn't confirmed.
return new BadRequestObjectResult("Email isn't confirmed.");
}
if (!await _userManager.IsPhoneNumberConfirmedAsync(user))
{
// Phone Number isn't confirmed.
return new BadRequestObjectResult("Phone Number isn't confirmed.");
}
return new BadRequestObjectResult("Login IsNotAllowed");
}
else if (result.IsLockedOut)
{
// Account is locked out.
return new BadRequestObjectResult("Account is locked out.");
}
else if (result.RequiresTwoFactor)
{
// 2FA required.
return new BadRequestObjectResult("2FA required");
}
else
{
// Password is incorrect.
return new BadRequestObjectResult("Password is incorrect.");
}
}
}
else
{
return new NotFoundObjectResult("Username is incorrect"); // User not found, return NotFound }
}
}
}
Controller:
public class HomeController : Controller
{
private readonly IUserService _userService;
public HomeController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLogin)
{
var result= await _userService.LoginAsync(userLogin);
return result;
}
}
Startup.cs:
Not sure what is _jwtManager.getJwtToken in your code,so I just guess it is an interface and owns a JwtManager class implemented this interface.And it contains a getJwtToken method which generated the token.
services.AddScoped<IUserService, UserService>();
services.AddScoped<IJwtManager, JwtManager>();

Global customization of error responses from .bet core 3.1 webapi

I'm using app.UseExceptionHandler("/error"); to customize the output of any unhandled exceptions.
However, validation errors use the framework (ApiController) and automatically return a default formatted error.
I see that I can overwrite this using InvalidModelStateResponseFactory but is there a way to have one global location to handle all of these?
I understand that one is for 500 responses and the other is 400 responses but I'd like to consolidate if possible.
If you just display simple error message,you could use UseStatusCodePagesWithReExecute and UseExceptionHandler middleware like below:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStatusCodePagesWithReExecute("/errors/{0}");
app.UseExceptionHandler("/errors/500");
//...
}
ErrorsController:
[ApiController]
[Route("[controller]")]
public class ErrorsController : ControllerBase
{
[Route("{code}")]
public IActionResult InternalError(int code)
{
return new ObjectResult(new ApiResponse(code));
}
}
Custom ApiResponse:
public class ApiResponse
{
public int StatusCode { get; }
public string Message { get; }
public ApiResponse(int statusCode, string message = null)
{
StatusCode = statusCode;
Message = message ?? GetDefaultMessageForStatusCode(statusCode);
}
private static string GetDefaultMessageForStatusCode(int statusCode)
{
switch (statusCode)
{
case 404:
return "Resource not found";
case 500:
return "An unhandled error occurred";
case 400:
return "Model error";
default:
return null;
}
}
}
How to test:
1.For 500:
[HttpGet]
public IActionResult Get()
{
throw new Exception("adasd");
}
2.For 400:
[HttpPost]
public IActionResult Get(TestModel model)
{
if (!ModelState.IsValid)
{
return BadRequest();//can't pass the ModelState to it
}
return Ok();
}
If you want to display the detailed ModelState error,you need to know that model validation is before action,so you should add an action filter based on my previous answer:
public class ApiValidationFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(new ApiBadRequestResponse(context.ModelState));
}
}
}
Custom ApiBadRequestResponse:
public class ApiBadRequestResponse : ApiResponse
{
public IEnumerable<string> Errors { get; }
public ApiBadRequestResponse(ModelStateDictionary modelState)
: base(400)
{
if (modelState.IsValid)
{
throw new ArgumentException("ModelState must be invalid", nameof(modelState));
}
Errors = modelState.SelectMany(x => x.Value.Errors)
.Select(x => x.ErrorMessage).ToArray();
}
}
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.Filters.Add(typeof(ApiValidationFilter));
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStatusCodePagesWithReExecute("/errors/{0}");
app.UseExceptionHandler("/errors/500");
//...
}
Result:
Reference:
https://www.devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api
You can using error middleware (refer my question here for that Or follow below code:
If you want to handle error in response then using OnActionExecuted insteed.
services.AddMvc(opt =>
{
opt.Filters.Add(typeof(ValidateModelStateAttributeFilter));
})
public class ValidateModelStateAttributeFilter : ActionFilterAttribute
{
/// <summary>
///
/// </summary>
public ValidateModelStateAttributeFilter()
{
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errors = context.ModelState.Values.Where(v => v.Errors.Count > 0)
.SelectMany(v => v.Errors)
.Select(v => v.ErrorMessage)
.ToList();
string combinError = errors.Any() ? string.Join("\n", errors) : string.Empty;
var responseObj = new DomainExceptionContract
{
Key = "BadRequest",
Message = combinError
};
context.Result = new JsonResult(new { Data = responseObj })
{
StatusCode = 400
};
}
}
}

Modelbinding an optional array of a custom model bound type

I'm stuck with binding an optional array in an ASP.NET Core Controller. The array contains elements of a custom type. Single elements of this type are bound with a custom model binder and validated in it.
Sample repo here: https://github.com/MarcusKohnert/OptionalArrayModelBinding
I get only two tests out of three working in the sample test project:
https://github.com/MarcusKohnert/OptionalArrayModelBinding/blob/master/OptionalArrayModelBindingTest/TestOptionalArrayCustomModelBinder.cs
public class TestOptionalArrayCustomModelBinder
{
private readonly TestServer server;
private readonly HttpClient client;
public TestOptionalArrayCustomModelBinder()
{
server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
client = server.CreateClient();
}
[Fact]
public async Task SuccessWithoutProvidingIds()
{
var response = await client.GetAsync("/api/values");
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task SuccessWithValidIds()
{
var response = await client.GetAsync("/api/values?ids=aaa001&ids=bbb002");
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task FailureWithOneInvalidId()
{
var response = await client.GetAsync("/api/values?ids=xaaa001&ids=bbb002");
Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
}
}
Controller:
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet]
public IActionResult Get(CustomIdentifier[] ids)
{
if (this.ModelState.IsValid == false) return this.BadRequest();
return this.Ok(ids);
}
}
Startup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new CutomIdentifierModelBinderProvider());
//options.ModelBinderProviders.Add(new CutomIdentifierModelBinderProvider());
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
}
ModelBinder:
public class CutomIdentifierModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
//if (context.Metadata.ModelType.IsArray && context.Metadata.ModelType == typeof(CustomIdentifier[]))
//{
// return new ArrayModelBinder<CustomIdentifier>(new CustomIdentifierModelBinder());
//}
if (context.Metadata.ModelType == typeof(CustomIdentifier))
{
return new BinderTypeModelBinder(typeof(CustomIdentifierModelBinder));
}
return null;
}
}
public class CustomIdentifierModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var attemptedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
var parseResult = CustomIdentifier.TryParse(attemptedValue);
if (parseResult.Failed)
{
bindingContext.Result = ModelBindingResult.Failed();
bindingContext.ModelState.AddModelError(bindingContext.ModelName, parseResult.Message.Message);
}
else
{
bindingContext.Model = parseResult.Value;
bindingContext.Result = ModelBindingResult.Success(parseResult.Value);
}
return Task.CompletedTask;
}
}
The MVC default ArrayModelBinder of T binds optional arrays correctly and sets ModelState.IsValid to true. If I use my own CustomIdentifierModelBinder however ModelState.IsValid will be false. Empty arrays are not recognized as valid.
How can I solve this problem? Thanks in advance.
You are very close. Just customize behavior of built-in ArrayModelBinder for the case of missing parameter. If extracted value is an empty string just fill the model with an empty array. In all other cases you could call usual ArrayModelBinder.
Here is a working sample that passes all your 3 tests:
public class CutomIdentifierModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType.IsArray && context.Metadata.ModelType == typeof(CustomIdentifier[]))
{
return new CustomArrayModelBinder<CustomIdentifier>(new CustomIdentifierModelBinder());
}
return null;
}
}
public class CustomArrayModelBinder<T> : IModelBinder
{
private readonly ArrayModelBinder<T> innerModelBinder;
public CustomArrayModelBinder(IModelBinder elemeBinder)
{
innerModelBinder = new ArrayModelBinder<T>(elemeBinder);
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var attemptedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
if (String.IsNullOrEmpty(attemptedValue))
{
bindingContext.Model = new T[0];
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
return innerModelBinder.BindModelAsync(bindingContext);
}
}
The solution is the following code change, reflected in this commit:
https://github.com/MarcusKohnert/OptionalArrayModelBinding/commit/552f4d35d8c33c002e1aa0c05acb407f1f962102
I've found the solution by inspecting MVC's source code again.
https://github.com/aspnet/Mvc/blob/35601f95b345d0ef938fb21ce1c51f5a67a1fb62/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinder.cs#L37
You'll need to check the valueProviderResult for None. If it's none then there is no parameter given and the ModelBinder binds correctly.
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
And also you register the provided ArrayModelBinder of T with your custom ModelBinder:
if (context.Metadata.ModelType.IsArray && context.Metadata.ModelType == typeof(CustomIdentifier[]))
{
return new ArrayModelBinder<CustomIdentifier>(new CustomIdentifierModelBinder());
}