0

I am trying to learn more about Entity Framework Core. Unforunately it appears to mandate a junction class when you create a many to many relationship. Please see the class structure below:

Person
Sport
PersonSport

A person has many Sports and a Sport has many Persons. When using EF I would put a Sport collection in the Person class and a Person collection in the Sport class. However, now I have to create the PersonSport class. Please see the API Controller below (this is the only code needed to replicate the problem):

namespace WebApplication1.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class PersonController : Controller
    {
        [Route("")]
        [ProducesResponseType(typeof(Person), (int)HttpStatusCode.OK)]
        //[ProducesResponseType(typeof(IEnumerable<Sport>), (int)HttpStatusCode.OK)]
        [HttpGet]
        public async Task<IActionResult> GetPerson()
        {
            Person p1 = new Person { Id = Guid.NewGuid() };
            Sport s1 = new Sport { Id = Guid.NewGuid(), Description = "Football" };
            Sport s2 = new Sport { Id = Guid.NewGuid(), Description = "Running" };
            PersonSport ps1 = new PersonSport { Person = p1, PersonId = p1.Id, Sport = s1, SportId = s1.Id };
            PersonSport ps2 = new PersonSport { Person = p1, PersonId = p1.Id, Sport = s2, SportId = s2.Id };
            List<PersonSport> personSports = new List<PersonSport>();
            personSports.Add(ps1);
            personSports.Add(ps2);
            p1.AssignSports(personSports);
            return Ok(p1);
        }
    }

    public class Entity
    {
        public Guid Id { get; set; }
    }

    public class Person : Entity
    {
        private readonly List<PersonSport> _personSports;
        public IReadOnlyCollection<PersonSport> PersonSports => _personSports.AsReadOnly();
        //public Guid Id { get; set; }

        public Person()
        {
            _personSports = new List<PersonSport>();
        }

        public void AssignSports(IEnumerable<PersonSport> personSports)
        {
            this._personSports.AddRange(personSports);
        }
    }

    public class Sport : Entity
    {
        //public Guid Id { get; set; }
        public string Description { get; set; }
    }

    public class PersonSport
    {
        public Person Person { get; set; }
        public Sport Sport { get; set; }
        public Guid PersonId { get; set; }
        public Guid SportId { get; set; }
    }
}

I debug the Web API project and navigate to: http://localhost:57320/api/Person and see this:

enter image description here

Notice the JSON is malformed. If I try to consume the Web API from a console app/unit test app; I see this error:

System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host
   --- End of inner exception stack trace ---
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.GetStringAsyncCore(Task`1 getTask)
   at ConsoleApp1.Program.CallWebAPI() 

How can I resolve this? The code I am using the consume the Web API is as follows:

HttpClient enquiryClient = new HttpClient();
                var responseString = await enquiryClient.GetStringAsync("http://localhost:57320/api/Person");
                var response = JsonConvert.DeserializeObject<Person>(responseString);
w0051977
  • 15,099
  • 32
  • 152
  • 329
  • Have you looked at your database after running this? If so, can you show the table definitions? I am asking since the Id's are commented, and I don't think EF's naming conventions can coupe with this. – Stefan Sep 03 '18 at 15:29
  • @Stefan, thanks however EF Core is not even implemented yet - I am just testing the web api. If you copy the code above into a .net core 2.1 web api project and debug it then you will see what I am mean (see screenshot). – w0051977 Sep 03 '18 at 15:31
  • @Stefan, I think the issue is a self referncing loop. – w0051977 Sep 03 '18 at 15:32
  • Ah, yes, that's possible, if that's the case, put a breakpoint at `return Ok` (or just before) and see what's happening. If it's serializable then the result should be ok. If it is circulair, than that might be a problem although a good serializer should be able to handle that. I don't think Web API has an issue with returning many to many relations. – Stefan Sep 03 '18 at 15:37
  • @Stefan, if I do what is suggested in the answer here then it is fixed: https://stackoverflow.com/questions/34753498/self-referencing-loop-detected-in-asp-net-core. However, Person.PersonSports.Person is null. Am I even approaching this correctly? – w0051977 Sep 03 '18 at 15:53
  • I was trying to write you some example code, but: since this is such a hypotheical case: it would be better to include EF: it will handle some of the cases for you. – Stefan Sep 03 '18 at 18:11

1 Answers1

0

I'll give you an example, but please note that this all would be a lot easier if you added the entity framework to your project.

To test the API, with your entities, this should work, but note: the pain lies within your entities:

public async Task<IActionResult> GetPerson()
{
    var person = new Person() {  Id = new Guid.NewGuid() };
    var sports = new [] {
                 new Sport() { Id = Guid.NewGuid(), Description = "Football" },
                 new Sport() { Id = Guid.NewGuid(), Description = "Soccer" } };

    person.AssignSports(sports);

    return Ok(person);
}

The rest of the stuff, with the virtuals, junction tables, data loading, navigation properties etc. will come when you have implemented entity framework.

For now, this will return a Person, with 2 sports.

Stefan
  • 17,448
  • 11
  • 60
  • 79
  • Thanks. However, I believe you have to assign a PersonSport to a Person rather than a Sport. Is that right? – w0051977 Sep 03 '18 at 18:37
  • On a side note; the answer for me was to add the following to the Startup: .AddJsonOptions(options => { //options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects; – w0051977 Sep 03 '18 at 18:39
  • To answer your first question, Yes, that is correct. (Still it's best to implement EF and see what will happen,) if you setup your entities right, you can assign a sport to a person. It has a more intuitive flow. Behind the scenes, EF will fill the junction table, although this depends on your setup. – Stefan Sep 04 '18 at 08:30
  • Another thing I want to point out is, that, since you are using an api, is that a cleaner approach would be to separate your domain models from your data layer. It's a bit of extra work, but you'll get a more maintainable solution. More info can be found here: https://stackoverflow.com/questions/23648832/viewmodels-in-mvc-mvvm-seperation-of-layers-best-practices – Stefan Sep 04 '18 at 08:32
  • I am using CQRS. Is it not normal practice to map the write database to the domain model? – w0051977 Sep 04 '18 at 19:29
  • Your link talks about MVVM, which I believe is a client side pattern. When you talk about isolating the domain model do you mean something like this: https://github.com/sapiens/ModernMembership/blob/master/src/ModernMembership/LocalMember.cs. Do you know any more examples? Have I understood what you are saying. Once I grasp this I will accept and upvote. – w0051977 Sep 05 '18 at 07:11
  • Hi, yes; it is good practice to map the database to the domain model; but not to include the database in your domain model. So: `database->datalayer/entities->domain model`. So the domain model doesn't have to be a 100% one to one mapping of your database. The article is really about separation of layers (which also applies to MVVM, MVC etc; but also to domain driven design). Using CQRS is really good: but only if you let it work with your domain models you are independent and separated of your data-layers. – Stefan Sep 05 '18 at 07:24
  • So, your `Entities` looks like entity framework entities. If so, than it's part of the data-layer, not the domain models. Note: for domain models to work; the need to have a self explanatory set of properties. And that's why I was triggered: I don't think your `PersonSports` has that kind of structure. The Ids itself don't have a value in a broader domain. It is also what made me think it's part of the data-layer. I hope all this makes a bit of sense. – Stefan Sep 05 '18 at 07:24
  • I am not familiar with the link you send but it seems it can be either a domain model or a data-layer-model. In any case: the whole model is well constructed and self explanatory. It contains full relevant details about the object (even has a sub-class in it); so it's a complete package and therefore has meaning in the broader domain... For example your junction `PersonsSports` table doesn't have that property. You should keep `PersonsSports` as data-table, but I wouldn't use it as domain model. – Stefan Sep 05 '18 at 07:35
  • Thanks. Do you mean something like this; where the respository gets a data object and then converts it into a domain object before returning it: http://www.dataworks.ie/Blog/Item/entity_framework_5_with_automapper_and_repository_pattern. You seem to suggest that mapping directly to the domain model is good practice and then say you should map to a data model as an intermediary between the ORM and domain model. Thanks again. – w0051977 Sep 05 '18 at 07:45
  • Ha, yes, the terminology can be confusing; there are several definitions which aren't always used in the correctly. Tip: don't use the repository pattern if you are using an ORM: the ORM already *IS* the repository pattern. So: `Database->ORM/Entities/Data-model->Domain model`, has 3 components, in which 1) is the database itself (no code/classes etc), 2) is the ORM, entity framework, entities, datalayer and whatever other name can be used and 3) is the domain which is used across application, exposed to 3th parties and used in the business logic. It's also the reslut and input of your CQRS. – Stefan Sep 05 '18 at 08:23
  • What is the difference between the data model and entities? Do you know of an example app that uses this approach e.g. on github. – w0051977 Sep 05 '18 at 09:52
  • That's my point with the terminology: data-model, data-layer, entities, ORM: Basically they are all the same: they all exist withini the data-layer context, which is close to your database. The things you expose through an API must be close to domain models; self explanatory objects. – Stefan Sep 05 '18 at 09:53
  • So the database maps to the domain model? In that case; how do you avoid the PersonSport entity and have a Person.Sports collection rather than a Person.personsports. – w0051977 Sep 05 '18 at 10:13
  • Define `maps`. In my definition it's a translation from one "thing" to another "thing"; so: `database` **maps to** `Entities/ORM` **maps to** `domain models` and back. – Stefan Sep 05 '18 at 11:42
  • Thanks. So an Entity class e.g. OrderEntity is a separate class to a domain class e.g. OrderDomain. Where do you map between OrderEntity and OrderDomain? – w0051977 Sep 05 '18 at 19:55
  • Yes, and normally they have a 90%+ overlap, so it seems extra work. Nevertheless; by doing this you can change one of them independent of the other, which is your separation of layers. And you have the benifit of not over exposing your database to the outside world. – Stefan Sep 06 '18 at 08:52
  • last question before I accept. If I am doing CQRS, then I would have a domain model; a write model (Entity classes mapped to DB) and a read model (Entity classes mapped to DB). Have I understood what you have said correctly. – w0051977 Sep 06 '18 at 08:58
  • Yes, let me give you an example; With CQRS, you have queries and commands. Your command may be a domain defined action. For example: `AddNotificationMethod`; if this command is defined within a broader domain; let's say cross application: it needs to be a domain-model since the entites might not be defined within all those applications. For the query it's the same: let say: `GetCar`, the result is a car (obviously). But is it a domain-car or a entity-car? : if you ever going to use the car within a different context: it should be a domain model. – Stefan Sep 07 '18 at 08:50
  • And note: if you are creating an API; you almost per definition have other applications involved in your setup. – Stefan Sep 07 '18 at 08:51