简体   繁体   中英

Searching in sql server json column and consume it using entity framework core

To make the story short, i have a model like this.

public class User : IEntity
{
    public int Id { get; set; }
    public string Properties { get; set; }
    public DateTime? CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
    [NotMapped]
    public UserDTO _Properties
    {
        get
        {
            return Properties == null ? null : JsonConvert.DeserializeObject<UserDTO>(Properties);
        }
        set
        {
            Properties = JsonConvert.SerializeObject(value);
        }
    }
}

And im storing json data inside the properties column, that part is ok, then i have a query where i need to search by Name or Email, so i have a Repository pattern and

alter procedure [dbo].[spSearchUsers]
@term nvarchar(max) AS BEGIN
select Id, JSON_VALUE(Properties, '$.FirstName') as FirstName,
    JSON_VALUE(Properties, '$.LastName') as LastName, 
    JSON_VALUE(Properties, '$.Email') as Email,
    JSON_VALUE(Properties, '$.Role') As [Role],
    JSON_VALUE(Properties, '$.ProgramInformation') as ProgramInformation,
    JSON_VALUE(Properties, '$.CertsAndCredentials') as CertsAndCredentials,
    JSON_VALUE(Properties, '$.Phone') as Phone,
    JSON_VALUE(Properties, '$.LogoUrl') as LogoUrl,
    JSON_VALUE(Properties, '$.ProfessionalPic') as ProfessionalPic,
    JSON_VALUE(Properties, '$.Biography') as Biography,
    CreatedAt, UpdatedAt
from Users
where CONCAT(JSON_VALUE(Properties, '$.FirstName'),' ',JSON_VALUE(Properties, '$.LastName')) like '%'+ @term +'%' 
    or JSON_VALUE(Properties, '$.Email') like '%'+ @term +'%'
    and JSON_VALUE(Properties, '$.IsActive') = 'true'
    and JSON_VALUE(Properties, '$.IsLockedOut') = 'false'
    and JSON_VALUE(Properties, '$.IsVerified') = 'true' END

When i execute the stored procedure inside the sql server management studio i get the information right:

exec spSearchUsers 'Raul'

I get this result:

1 Raul Alejandro Baez Camarillo raul.baez.camarillo@gmail.com 0 NULL NULL NULL NULL NULL NULL 2021-11-16 03:08:09.6630000 2021-11-16 03:08:09.6630000

So now i want to consume that stored procedure in my controller, im using Repository pattern and i have tried using the context.Model.FromSqlRaw like this.

var result = await _context.Users.FromSqlRaw("EXEC dbo.spSearchUsers {0}", term).ToListAsync()

But when i execute it im getting this error:

System.InvalidOperationException: The required column 'Properties' was not present in the results of a 'FromSql' operation. at Microsoft.EntityFrameworkCore.Query.Internal.FromSqlQueryingEnumerable 1.BuildIndexMap(IReadOnlyList 1 columnNames, DbDataReader dataReader) at Microsoft.EntityFrameworkCore.Query.Internal.FromSqlQueryingEnumerable 1.AsyncEnumerator.InitializeReaderAsync(DbContext _, Boolean result, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func 4 operation, Func 4 verifySucceeded, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Query.Internal.FromSqlQueryingEnumerable 1.AsyncEnumerator.MoveNextAsync() at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable 1 source, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable 1 source, CancellationToken cancellationToken) at AcadminRest.Controllers.UsersController.Search(String term) in D:\\Projects\\Accomplishment\\AcadminRest\\AcadminRest\\Controllers\\UsersController.cs:line 71 at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)

Can someone help me getting this? thanks in advance.

Well - you could use Value conversion:

https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions?tabs=data-annotations#composite-value-objects

Another option would be to create a keyless entity for that specific query:

https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types?tabs=data-annotations

Edit: here's a bit on how to setup a keyless entity. https://stackoverflow.com/a/69924584/4122889

The options that sommmen gave are ones that I would have also offered up. In particular -- If you're not tied to your procedure at all.

Both options give you a simpler querying method within your repository. Personally -- I would go with the value conversion but... here are both options in more detail.

Value conversion: you would want to use your UserDTO and actually map it. Your model would look like so:

public class User : IEntity
{
    public int Id { get; set; }
    public UserDTO Properties { get; set; }
    public DateTime? CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
}

No need for an extra property. Then your db context would be updated to use a conversion for Properties :

// somewhere within OnModelCreating
modelBuilder.Entity<User>(entity =>
{
    entity.Property(r => r.Properties)
        .HasConversion(
            v => JsonConvert.SerializeObject(v),
            v => JsonConvert.DeserializeObject<UserDTO>(v)
        );
});

Now in your repository you can search with linq on your Users .

var result = await _context.Users
    .Where(u => u.Properties.FirstName.Contains(term) ||
                u.Properties.LastName.Contains(term) || 
                u.Properties.Email.Contains(term))
    .Where(u => u.IsActive && !u.IsLockedOut && u.IsVerified).ToListAsync();

-- OR --

The other option (but slightly different from just a keyless entity) - if you want to make use of the query that you already have in your procedure.
Create a view ( UserDTO_Properties ) with your query w/o the concatenation of first/last fields and then a keyless entity to go with your view.

You would take your UserDTO and create the keyless entity to map to your view. In your db context:

    // context prop for dbset
    DbSet<UserDTO> UserDTOs { get; set; }

    // and then somewhere in OnModelCreating
    modelBuilder.Entity<UserDTO>(entity =>
    {
        entity.ToTable("UserDTO_Properties");
        entity.HasNoKey();
    }

Now a similar query to get your results:

var result = await _context.UserDTOs
    .Where(u => u.FirstName.Contains(term) ||
                u.LastName.Contains(term) || 
                u.Email.Contains(term)).ToListAsync();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM