JavaScriptSerializer circular reference when using ScriptIgnore
Asked Answered
S

2

13

I have my Entity Framework Entities split out into a separate class library from my web project and data access layer. In my controller I make a call to my repository to get an IEnumerable<RobotDog.Entities.Movie> and then try to serialize into json using JavaScriptSerializer but I get a circular reference even though I'm using the [ScriptIgnore] attribute.

IMPORTANT: Originally I had my entities, data access and web all under one project and I was able to successfully serialize my entites without a circular reference. When I created separate layers that's when I started having problems. I did not change any of the entities.

An example of one of my entities in the RobotDog.Entities namespace:

namespace RobotDog.Entities {
    public class Character {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }

        [MaxLength(200)]
        public string Name { get; set; }

        public virtual Person Person { get; set; }

        [ScriptIgnore]
        public virtual Movie Movie { get; set; }
    }
}

My controller:

namespace RobotDog.Web.Controllers {
    public class MoviesController : Controller {
        private UnitOfWork _unitOfWork = new UnitOfWork();

        [HttpGet]
        public ActionResult Index() {
            var user = Membership.GetUser(User.Identity.Name);
            if(user != null) {
                var movies = _unitOfWork.UserMovieRepository.Get(u => u.UserId == (Guid) user.ProviderUserKey).Select(m => m.Movie);
                var serializer = new JavaScriptSerializer();
                var json = serializer.Serialize(movies);
                return View(json);
            }
            return View();
        }

    }
}

My Repository:

namespace RobotDog.DataAccess.Movies {
    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class {
        internal MovieContext Context;
        internal DbSet<TEntity> DbSet;

        public Repository(MovieContext context) {
            if (context == null)
                throw new ArgumentNullException("context");

            Context = context;
            DbSet = Context.Set<TEntity>();
        }

        public virtual IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> predicate = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null ) {
            IQueryable<TEntity> query = DbSet;

            if (predicate != null)
                query = query.Where(predicate);

            return orderBy != null ? orderBy(query).ToList() : query.ToList();
        }
    }
}
Social answered 28/1, 2013 at 19:2 Comment(2)
Most people switch to JSON.net at that point. The other route would be to write your own javascriptserializer implementation by overriding the section which allows the circular reference.Altimeter
Yeah I looked into JsonConvert but I didn't like the idea of having to write more code when it should work, and originally was working before I separated into multiple projects, by just decorating a property with [ScriptIgnore]. I've also looked into creating a viewModel and then using AutoMapper to map the two types. But I just can't wrap my head around having to write more code to do what should be done with no code. Plus, not being able to figure out why this isn't working is driving my crazy :)Social
D
9

Circular object graphs cannot be JSON serialized. And when you give it a second thought it actually makes sense. The correct way to handle this is to use view models. You should never pass your domain entities directly to your views. Always define a view model containing only the necessary properties that you want to be exposed.

I am sure that the client consuming this JSON doesn't care about having this circular object graph. So simply define a view model breaking this circular dependency and including only the properties you need.

Then all you have to do is map your domain model to the view model and pass this view model to a JsonResult (yeah that's another issue in your code - you are manually JSON serializing and writing plumbing code in your controller action instead of delegating this to the framework).

So:

[HttpGet]
public ActionResult Index() 
{
    var user = Membership.GetUser(User.Identity.Name);
    if(user != null) 
    {
        IEnumerable<Movie> movies = _unitOfWork
            .UserMovieRepository.Get(u => u.UserId == (Guid) user.ProviderUserKey)
            .Select(m => m.Movie);
        IEnumerable<MovieViewModel> moviesVm = ... map the domain model to your view model
        return Json(moviesVm, JsonRequestBehavior.AllowGet);
    }

    // return an empty movies array
    var empty = Enumerable.Empty<MovieViewModel>();
    return Json(empty, JsonRequestBehavior.AllowGet);
}

The important thing you should be focusing right now on is defining the MovieViewModel class which will contain only the information that you want to expose to the client as JSON. Break all circular references. Feel free to have additional view models that this main view model is referencing in order to map other entities.

And most importantly : never pass your domain models to the view. Always define view models. This way your application is completely independent of the underlying data access technology you are using. You could modify your DAL layer as much as you like without impacting the UI part because this UI is represented by view models.

Darceldarcey answered 30/1, 2013 at 21:14 Comment(5)
I had actually gone down this path. I've written the viewModels out already and I was going to use AutoMapper to map my viewModels to my domain models. I know this will work. What I don't understand is why [ScriptIgnore] worked before I broke my project out into multiple tiers (ie: Web, Business, DAL) and now it wont. And with the exception of the properties that have circular dependencies, all other properties should be serialized to json - so I figured creating a viewModel was just overkill.Social
What your saying is that it's best practice to keep the domain models separate from UI even if it's a 1-to-1 mapping and no properties need to be excluded/changed?Social
Yeah absolutely, that's exactly what I am saying. Of course if it is a 1-to-1 mapping and you have circular references in your domain model you will have the same problem if you use view models. In a real world application it is seldom 1-to-1 mapping.Darceldarcey
Would you suggest creating my own mapper or use a third party dll like Automapper, which I imagine uses reflection to auto map members?Social
I wouldn't suggest you creating your own mapper and reinventing the wheels. AutoMapper uses Reflection but it uses it intelligently by caching expensive operations. You should absolutely not be worried about the performance with AutoMapper. I am using it in some very high traffic applications in production and the mapping overhead is negligible compared to other parts of the execution of the MVC pipeline.Darceldarcey
U
25

Maybe kinda late response, but I had similar problem with POCO Classes for Entity Framework Code-Firts. The problem was that may properties were declared as virtual. In this case EF creates proxy class which overrides the virtual property. It seems that ScriptIgnore attribute is not by default applied on overriden properties, unless you use it like this:

[ScriptIgnore(ApplyToOverrides=true)]
Unquiet answered 14/6, 2013 at 12:59 Comment(5)
Yes, this is what I needed. Thanks! Too bad this is not the approved answer.Fountainhead
where should i write above line of code ? i have same problemLoralyn
Awesome, this really helped me, got my head out of this issue from past 6 hours. Thank you so much !!!Delayedaction
Thank you! There is definately a non-zero cost for maintaining a ton of view models. There are places where this is the right answer. Not always, but definately sometimes.Succory
This is definitely the way to go.Ferial
D
9

Circular object graphs cannot be JSON serialized. And when you give it a second thought it actually makes sense. The correct way to handle this is to use view models. You should never pass your domain entities directly to your views. Always define a view model containing only the necessary properties that you want to be exposed.

I am sure that the client consuming this JSON doesn't care about having this circular object graph. So simply define a view model breaking this circular dependency and including only the properties you need.

Then all you have to do is map your domain model to the view model and pass this view model to a JsonResult (yeah that's another issue in your code - you are manually JSON serializing and writing plumbing code in your controller action instead of delegating this to the framework).

So:

[HttpGet]
public ActionResult Index() 
{
    var user = Membership.GetUser(User.Identity.Name);
    if(user != null) 
    {
        IEnumerable<Movie> movies = _unitOfWork
            .UserMovieRepository.Get(u => u.UserId == (Guid) user.ProviderUserKey)
            .Select(m => m.Movie);
        IEnumerable<MovieViewModel> moviesVm = ... map the domain model to your view model
        return Json(moviesVm, JsonRequestBehavior.AllowGet);
    }

    // return an empty movies array
    var empty = Enumerable.Empty<MovieViewModel>();
    return Json(empty, JsonRequestBehavior.AllowGet);
}

The important thing you should be focusing right now on is defining the MovieViewModel class which will contain only the information that you want to expose to the client as JSON. Break all circular references. Feel free to have additional view models that this main view model is referencing in order to map other entities.

And most importantly : never pass your domain models to the view. Always define view models. This way your application is completely independent of the underlying data access technology you are using. You could modify your DAL layer as much as you like without impacting the UI part because this UI is represented by view models.

Darceldarcey answered 30/1, 2013 at 21:14 Comment(5)
I had actually gone down this path. I've written the viewModels out already and I was going to use AutoMapper to map my viewModels to my domain models. I know this will work. What I don't understand is why [ScriptIgnore] worked before I broke my project out into multiple tiers (ie: Web, Business, DAL) and now it wont. And with the exception of the properties that have circular dependencies, all other properties should be serialized to json - so I figured creating a viewModel was just overkill.Social
What your saying is that it's best practice to keep the domain models separate from UI even if it's a 1-to-1 mapping and no properties need to be excluded/changed?Social
Yeah absolutely, that's exactly what I am saying. Of course if it is a 1-to-1 mapping and you have circular references in your domain model you will have the same problem if you use view models. In a real world application it is seldom 1-to-1 mapping.Darceldarcey
Would you suggest creating my own mapper or use a third party dll like Automapper, which I imagine uses reflection to auto map members?Social
I wouldn't suggest you creating your own mapper and reinventing the wheels. AutoMapper uses Reflection but it uses it intelligently by caching expensive operations. You should absolutely not be worried about the performance with AutoMapper. I am using it in some very high traffic applications in production and the mapping overhead is negligible compared to other parts of the execution of the MVC pipeline.Darceldarcey

© 2022 - 2024 — McMap. All rights reserved.