1

Here's the code: controller:


        //POST api/substances
        [HttpPost]
        [ServiceFilter(typeof(ValidateNameExistsAttribute<Substance>))]
        public ActionResult<SubstanceReadDto> CreateSubstance([FromBody]SubstanceSaveDto dto)
        {
            var substanceModel = _mapper.Map<Substance>(dto);
            _repository.CreateSubstance(substanceModel);
            _repository.SaveChanges();

            var substanceReadDto = _mapper.Map<SubstanceReadDto>(substanceModel);

            return CreatedAtRoute(nameof(GetSubstanceById), new {substanceReadDto.Id}, substanceReadDto);
        }

Filter

using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Pharmacy.Data;
using Pharmacy.Models;

namespace Pharmacy.Filters
{
    public class ValidateNameExistsAttribute<T> : IActionFilter where T : class, INameEntity
    {
        private readonly PharmacyContext _context;

        public ValidateNameExistsAttribute(PharmacyContext context)
        {
            _context = context;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            if (context.ActionArguments.ContainsKey("dto"))
            {
                var entity = new object();

                var dto = context.ActionArguments["dto"] as INameDto;
                if (dto == null)
                {
                    context.Result = new BadRequestObjectResult("Invalid request body");
                    return;
                }

                if (context.ActionArguments.ContainsKey("id"))
                {
                    var id = (int) context.ActionArguments["id"];
                    entity = _context.Set<T>().SingleOrDefault(it => String.Equals(it.Name, dto.Name, StringComparison.OrdinalIgnoreCase) && it.Id != id);
                }
                else
                {
                    entity = _context.Set<T>().SingleOrDefault(it => String.Equals(it.Name, dto.Name, StringComparison.OrdinalIgnoreCase));
                }

                if (entity != null)
                {
                    var problemDetails = new ProblemDetails
                    {
                        Title = "Duplicate resource",
                        Detail = $"A record with provided name {dto.Name} already exists",
                        Instance = context.HttpContext.Request.Path
                    };

                    context.Result = new ObjectResult(problemDetails)
                    {
                        StatusCode = 409
                    };
                }
            }
        }
    }
}

and here's the error message: What's wrong with code?

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.6.1", "title": "The LINQ expression 'DbSet\r\n .Where(s => string.Equals(\r\n a: s.Name, \r\n b: __dto_Name_0, \r\n comparisonType: OrdinalIgnoreCase))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.", "status": 500, "detail": " at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.g__CheckTranslated|8_0(ShapedQueryExpression translated, <>c__DisplayClass8_0& )\r\n at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)\r\n at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)\r\n at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)\r\n at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)\r\n at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)\r\n at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)\r\n at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)\r\n at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)\r\n at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)\r\n at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_01.<Execute>b__0()\r\n at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func1 compiler)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func1 compiler)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)\r\n at System.Linq.Queryable.SingleOrDefault[TSource](IQueryable1 source, Expression1 predicate)\r\n at Pharmacy.Filters.ValidateNameExistsAttribute1.OnActionExecuting(ActionExecutingContext context) in D:\.Pharmac Project\pharmac\Filters\ValidateNameExistsAttribute.cs:line 43\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)\r\n at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)\r\n at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)\r\n at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)\r\n at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\r\n at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)\r\n at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)\r\n at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)", "traceId": "|9542c43b-43548354a97cc054." }

Apearl
  • 11
  • 3
  • The error is clear. What you tried can't be translated to SQL. SQL comparisons are affected by a column's collation and most of the time, databases use a case-*in*sensitive collation. The available collations depend on the database product but none of them has an `Ordinal` collation. There are binary collations, with their own characteristics. *Indexes* depend on the stored data's collation as well, so trying to change the comparison behavior can cause severe performance issues. – Panagiotis Kanavos Mar 24 '21 at 13:28
  • *Remove* `String.Equals` and just compare the fields. – Panagiotis Kanavos Mar 24 '21 at 13:32
  • I removed. it warns:**`Cannot convert expression type '(string, string, System.StringComparison OrdinalIgnoreCase)' to return type 'bool'`** and `'Ambiguous invocation: Pharmacy.Models.Agent SingleOrDefault(this System.Collections.Generic.IEnumerable, System.Func) (in class Enumerable) Pharmacy.Models.Agent SingleOrDefault(this System.Linq.IQueryable, System.Linq.Expressions.Expression>) (in class Queryable) match'`. – Apearl Mar 24 '21 at 13:40
  • ```(it => it.Name.Equals(dto.Name, StringComparison.OrdinalIgnoreCase)``` also doesn;t solve the problem – Apearl Mar 24 '21 at 13:40
  • Of course it doesn't, it's the same code you already used! Remove it. Use `it.Name==dto.Name` – Panagiotis Kanavos Mar 24 '21 at 13:40
  • It works, but case-insensitivity doesn't apply then . e.g. Coffee and COFFEE are created at db sidevinstead of "Resource already exists" notification. I don't want to use ToLower(), instead just want to use built-in StringComparison.OrdinalIgnoreCase – Apearl Mar 24 '21 at 13:57
  • It's the "Ordinal" bit the DB can't handle, not necessary the "IgnoreCase" bit... – canton7 Mar 24 '21 at 14:01
  • @HalimHamidov I already explained why that happens, and why you can't change it from the client. If you want case-insensitive search, make sure the field's collation is case-insensitive. Again, collation affects indexes so if you try to use a different collation (it's possible) you *won't* be able to use any indexes and end up scanning the entire table – Panagiotis Kanavos Mar 24 '21 at 14:03
  • @HalimHamidov that's covered in the documentation as well: [Collations and Case Sensitivity](https://learn.microsoft.com/en-us/ef/core/miscellaneous/collations-and-case-sensitivity). This page explains about collations, how to change them with `EF.Collate`, why this harms performance, and how EF Core can map specific overloads of `String.Equals` but that's *still* a bad idea – Panagiotis Kanavos Mar 24 '21 at 14:05
  • changed to Ordinal. Again the above-mentioned error. – Apearl Mar 24 '21 at 14:06
  • @HalimHamidov you keep using the incorrect code. You'll keep getting the same error. No matter how many alternatives of that *bad* comparison you use, it will still fail – Panagiotis Kanavos Mar 24 '21 at 14:08
  • @HalimHamidov if you have different types of comparisons, you usually need different columns, with different collations (one case-insensitive, one case-sensitive). Very few databases support multiple collations per field, and even those that do, like CosmosDB, they actually use a separate column with the second collationo – Panagiotis Kanavos Mar 24 '21 at 14:10
  • @PanagiotisKanavos Thanks, and what's then the best idea & good practice in my case? – Apearl Mar 24 '21 at 14:12
  • Shortly, For now is it better to use `it.Name.ToLower() == dto.Name.ToLower()`? – Apearl Mar 24 '21 at 14:18
  • I already explained the options, even provided a link to the docs. Either change the column's collation to be case-*in*sensitive (why is it CS anyway?) or use a duplicate column with a Case-Insensitive collation. Use `EF.Collate` only if you have very little data. `OrdinalIngoreCase` is a contradiction for most databases - `Ordinal` translates to a binary collation (ie comparing bytes directly) so there's no upper or lower case. If the byte values are different, the strings are different. Using `ToLower()` is the worst possible option – Panagiotis Kanavos Mar 24 '21 at 14:19

0 Answers0