Breeze / breeze.server.net

Breeze support for .NET servers
MIT License
76 stars 62 forks source link

AspnetCore no ODataQueryOptions, how to expand Automapper? #51

Closed randellhodges closed 2 years ago

randellhodges commented 7 years ago

I am attempting to use Automapper to use DTOs in the Aspnet Core version of Breeze. The difficulty I'm am facing right now is that I can't use expand. If I do, it won't generate a good query and I get an error.

The aspnet classic (?) approach using ODataQueryOptions doesn't seem to be an option now because of the decoupling from OData.

Link 1 Link 2

Is there a way to get this information so I can pass it along to automapper, or is this more of a feature request?

steveschmitt commented 7 years ago

Can you give an example of the query and the expand that you're using, and the error?

randellhodges commented 7 years ago

The error:

InvalidOperationException: The result type of the query is neither an EntityType nor a CollectionType with an entity element type. An Include path can only be specified for a query with one of these result types.

Snip from the api side:

[Authorize]
[HttpGet]
public async Task<IActionResult> UserProfile()
{
    var userId = _userManager.GetUserId(HttpContext.User);

    if (userId != null)
    {
        var list = await Task.Run(() =>
        {
            return _manager
                .Context
                .UserProfiles
                .Where(p => p.UserId == userId)
                //.ProjectTo<UserProfile>(_mapper.ConfigurationProvider, (p) => p.Characters);
                .ProjectTo<UserProfile>(_mapper.ConfigurationProvider);
        });

        return Ok(list);
    }

    return Unauthorized();
}

Snip from the javascript side:

service.loadProfile = function () {

    var deferred = $q.defer();

    breeze
        .EntityQuery
        .from('UserProfile')
        .expand('characters')
        .using(manager)
        .execute()
        .then(function (data) {
            service.profile = data.results[0];
            deferred.resolve();
        });

    return deferred.promise;
};

If I omit the .expand('characters') on the client and then use the overload .ProjectTo<UserProfile>(_mapper.ConfigurationProvider, (p) => p.Characters); on the server, it works. I think it is because the order things are applied. It is trying to apply the expand after Automapper projected it in the DTO.

Oh, and inside Automapper, I use this option to prevent Automapper for causing the expand. If I don't use this, Automapper will cause the expand, regardless of anyone client or server, doing an include.

.ForMember(d => d.Characters, o => { o.MapFrom(s => s.CharacterProfiles); o.ExplicitExpansion(); });

If I may... While I'm at it (sorry about muddying the waters with this second question), since I have the code showing, this line doesn't seem to work as I would expect:

service.profile = data.results[0];

That is the raw object. I'm using AngularJS (1.6 whatever) and I'm trying to make that a property of my service. It works, but it isn't a "Breeze" object, so change tracking doesn't work. I think I'm doing that all wrong, but I haven't found any good examples yet. (And, I am including the adapters/breeze.bridge.angular.js script)

marcelgood commented 7 years ago

Yeah, this won't work. You are doing a projection, so the shape of your query are not entities, but DTOs as you already recognized yourself. Specifying expand on the client only works if the shape of your query are entities. This is an EF issue, it can't apply the expand to DTOs, only to the underlying entities. This is also why after all is said and done you don't have change tracking on the client. Breeze doesn't know what to do with DTOs. They are just plain objects. They don't have any metadata, so Breeze just hands you the raw object. If you want Breeze to add its change tracking logic etc. to the object, then you need to describe your DTOs in the metadata store.

Lastly, just out of curiosity, why the Task.Run? What are you trying to accomplish with this? The server is already handling requests on multiple threads.

randellhodges commented 7 years ago

Correct. That is why I was thinking, if there was some way to get the expand parameters and let me pass those along to automapper, similar to how you could use ODataQueryOptions in the function in the WebApi method of doing it.

Could you expand on the "decribe your DTOs in the metadata store" statement? I think I am doing that, but maybe I'm missing something.

I am using the Entity Framework as a metadata design tool method. I basically copied my EF objects and made a couple changes. I have a custom context that is used to return the metadata. Maybe I've gotten something wrong there. I am digging into that.

Edit: Not sure if it matters, but the json returned from Metadata is completely different than the example at the bottom of Metadata schema.

Edit 2: Oh, Task.Run, just an artifact of trying different things.

marcelgood commented 7 years ago

You can use EntityQuery.withParameters to send any kind of parameters along with your query that get passed to your server method.

http://breeze.github.io/doc-js/api-docs/classes/EntityQuery.html#method_withParameters

By describing the DTOs, I mean you have to create metadata for them, so that Breeze thinks they are entities. Yes, the metadata design tool is one way to do it. You may have a mistmatched type name or something so Breeze doesn't find a match in the metadata store and therefore just returns the raw object to you.

randellhodges commented 7 years ago

I hate to be annoying, but for the life of me, no matter what I do, I only get an array of plain objects in the results.

I removed the DTO, returned the EF models directly, based on Test.AspNetCore.Controllers.NorthwindIBModelController

I have a custom class that inherits from EFPersistenceManager using my original EF DbContext. I don't do anything in that class (yet).

MyApp.Data.dll

namespace MyApp.Data
{
    // T4 generated
    public partial class MyContext : DbContext
    {
        ...
    }
}
namespace MyApp.Data.Models
{
    // T4 generated
    public partial class UserProfile
    {
        ...
    }
}

MyApp.Web.dll

namespace MyApp.Web
{
    public class MyPersistanceManager : EFPersistenceManager<MyContext>
    {
        public MyPersistanceManager(MyContext context) : base(context)
        {
        }
    }
}
namespace MyApp.Web.Controllers
{
    public class ProfileController : Controller
    {
        MyContext _context;
        MyPersistanceManager _manager;

        ApplicationUserManager _userManager;

        public ProfileController(
            MyContext dbContext,
            ApplicationUserManager userManager
        )
        {
            _context = dbContext;
            _manager = new MyPersistanceManager(dbContext);

            _userManager = userManager;
        }

        [HttpGet]
        public IActionResult Metadata()
        {
            return Ok(_manager.Metadata());
        }

        [Authorize]
        [HttpGet]
        public IQueryable<Data.Models.UserProfile> UserProfile()
        {
            var userId = _userManager.GetUserId(HttpContext.User);

            if (userId != null)
            {
                return _manager
                        .Context
                        .UserProfiles
                        .Where(p => p.UserId == userId);
            }

            return null;
        }
    }
}

javascript:

service.loadProfile = function () {

    var deferred = $q.defer();

    breeze
        .EntityQuery
        .from('UserProfile')
        .using(manager)
        .execute()
        .then(function (data) {
            service.profile = data.results[0];
            deferred.resolve();
        });

    return deferred.promise;
};

The data.results is just a plain array.

randellhodges commented 7 years ago

I think my problem is the json that is being returned from the web service. I do not have an $id or $type property in the json response like JsonResultsAdapter

randellhodges commented 7 years ago

Ok, I found my issue. Completely my fault.

I didn't have my formatter configured in Startup. The following seems to solve the issue with using the EF models directly. I think it'll probably fix my DTO issue (for change tracking) as well.

mvcBuilder.AddJsonOptions(opt => {
    var ss = JsonSerializationFns.UpdateWithDefaults(opt.SerializerSettings);
    var resolver = ss.ContractResolver;
    if (resolver != null) {
        var res = resolver as DefaultContractResolver;
        res.NamingStrategy = null;  // <<!-- this removes the camelcasing
    }
});

mvcBuilder.AddMvcOptions(o => { o.Filters.Add(new GlobalExceptionFilter()); });
randellhodges commented 7 years ago

Just to clarify, I resolved the 2nd issue I had, the one that wasn't related to the original title. I knew I should not have included it here and get off track.

withParameters might work, but it would still be a lot more flexible if there was a way to get a parsed object like ODataQueryOptions that we could work with inside AspnetCore.