locked
NET5 Web API + EF Core - Entity Model vs Custom Model for HTTP POST RRS feed

  • Question

  • User379720387 posted

    This thread pretty much has the lead in to my conondrum which is should I have my POST controller action receive the Entity Model or a Custom Model.

    https://forums.asp.net/p/2175595/6336361.aspx?p=True&t=637536684536156425

    Entity Model path:

    object cycle detected error message

    the ReferenceHandler.Preserve option is needed for json serialization

    end up with stray related Entities in the Entity Model that I don't want which adds fragment like these:

    "ProviderRxTransactions": {
        "$id": "6",
        "$values": []
      },

    try to mitigate by doing something like this, but doesn't seem to work because the fragments are still there

    txnNew.ProviderRxTransactions.Add(null);

    which result in a bad request because it cannot process what is there when posting to the POST action controller

    Custom Model path:

    the related Entities are added to custom Transaction Model on an as needed basis

    code hits the POST controller action

    but burps on internal server error 500, because my custom transaction model is NOT  Transaction the Entity model

    I don't know if there is a convenient way to convert model my custom Transaction Model to the Entity Transaction Model. Is there?

    Then I end up parsing the custom Transaction model to the Transaction Entity Model in the POST controller action and I end up with a lot of code duplication, and monkey code.

    You all seem to like Web API, but I cannot imagine you would tolerate all this duplication and monkey code.

    What the solution to my conondrum?

    Sunday, April 11, 2021 2:03 PM

All replies

  • User-474980206 posted

    The webapi should be an abstraction of the underlying database. There may be internal data and relationships you never want to expose via webapi. So generally I never use entity objects as webapi parameters. I create request and response objects. A set of helper mappers (usually written in linq) allows not duplicating the mapping. 

    also using transaction objects  focus on the intent of the update rather than the structure of the database.

    Sunday, April 11, 2021 2:45 PM
  • User379720387 posted

    I create request and response objects.

    Ok, I think my TxnNew model is a request object, correct?

    A response object would come into play like this?

    context.Add(newTxn);
    await context.SaveChangesAsync();
                    
    //take some stuff from newTxn and put it in newResponseTxn
    
    return Ok(newReponseTxn);

    Yes?

    Sunday, April 11, 2021 4:08 PM
  • User1120430333 posted

    EF Entity model objects should not travel period,  IMHO. What should travel between the WebAPI service and WebAPI client is the DTO. In a layered or n-tier styled solution where EF is being used in the data access layer (DAL) as an example,  the EF Entities are left behind and the DTO(s) are sent. A complete disconnect from whatever is happening with EF.

    Data transfer object - Wikipedia

    Create Data Transfer Objects (DTOs) | Microsoft Docs

    Data Transfer Object Design Pattern in C# - CodeProject

    The DtoCache example that was shown to you the DTOCache was sent back to the WebAPI client side program from the WebAPI service.

    The example Github solution clearly shows the usage of the DTO pattern used between the Razor project (named Blazor), the Service layer classlib project, the WebAPI project and the DAL classlib project. They all have reference to the Entities classlib project where the DTO(s) are kept, and they know about the DTO(s).

    darnold924/PubCompanyCore3.x (github.com)

    There is nothing stopping you from having one DTO container object holding multiple objects List<T>(s) of DTO(s) or whatever other type of object that is a data object within the container DTO, and send the container DTO back and forth not involving the EF model objects

    The DtoCache object I showed in a reply post to you is doing just that, leaving the EF model objects behind. And on that same token, if I needed to send the DtoCache object back on a post, I could easily Json serialized it and sent it.

    using DAL;
    using Entities;
    using Microsoft.AspNetCore.Mvc;
    
    namespace ProgMgmntCore2Api.Controllers
    {
        [Produces("application/json")]
        [Route("api/[controller]")]
        [ApiController]
        public class CacheController : ControllerBase,  ICacheController
        {
            private readonly IDaoCache _daoCache;
    
            public CacheController(IDaoCache daoCache)
            {
                _daoCache = daoCache;
            }
    
            [HttpGet]
            public CacheResponse Get_Cache()
            {
                var resp = new CacheResponse();
    
                var cache = _daoCache.GetCache();
    
                resp.DtoCache.Durations = cache.Durations;
                resp.DtoCache.ProjectTypes = cache.ProjectTypes;
                resp.DtoCache.Resources = cache.Resources;
                resp.DtoCache.Statuses = cache.Statuses;
    
                return resp;
            }
        }
    }
    using System.Collections.Generic;
    using System.Linq;
    using DAL.Models.DB;
    using Entities;
    using Microsoft.Extensions.Options;
    
    namespace DAL
    {
        public class DaoCache : IDaoCache
        {
            private readonly IOptions<ConnectionStrings> _options;
          
            public DaoCache(IOptions<ConnectionStrings> options)
            {
                _options = options;
            }
    
            public DtoCache GetCache()
            {
                var dtocache = new DtoCache
                {
                    ProjectTypes = new List<DtoProjectType>(),
                    Statuses = new List<DtoStatus>(),
                    Resources = new List<DtoResource>(),
                    Durations = new List<DtoDuration>()
                };
    
                using (var context = new ProjectManagementContext(_options))
                {
                    var projectypes = (from a in context.ProjectTypes select a).ToList();
                    CreateProjectTypes(dtocache, projectypes);
    
                    var statuses = (from a in context.Statuses select a).ToList();
                    CreateStatuses(dtocache, statuses);
    
                    var resources = (from a in context.Resources select a).ToList();
                    CreateResources(dtocache, resources);
    
                    var durations = (from a in context.Durations select a).ToList();
                    CreateDurations(dtocache, durations);
                }
    
                return dtocache;
            }
    
            private static void CreateProjectTypes(DtoCache dtocache, List<ProjectTypes> projectypes)
            {
                foreach (var pt in projectypes)
                {
                    var dto = new DtoProjectType
                    {
                        ProjectTypeId = pt.ProjectTypeId,
                        Text = pt.Text,
                        Value = pt.Value
                    };
    
                    dtocache.ProjectTypes.Add(dto);
                }
            }
    
            private static void CreateStatuses(DtoCache dtocache, List<Statuses> statuses)
            {
                foreach (var st in statuses)
                {
                    var dto = new DtoStatus()
                    {
                        StatusId = st.StatusId,
                        Text = st.Text,
                        Value = st.Value
                    };
    
                    dtocache.Statuses.Add(dto);
                }
            }
    
            private static void CreateResources(DtoCache dtocache, List<Resources> resources)
            {
    
                foreach (var rc in resources)
                {
                    var dto = new DtoResource()
                    {
                        ResourceId = rc.ResourceId,
                        Text = rc.Text,
                        Value = rc.Value
                    };
    
                    dtocache.Resources.Add(dto);
                }
            }
    
            private static void CreateDurations(DtoCache dtocache, List<Durations> durations)
            {
    
                foreach (var du in durations)
                {
                    var dto = new DtoDuration()
                    {
                        DurationId = du.DurationId,
                        Text = du.Text,
                        Value = du.Value
                    };
    
                    dtocache.Durations.Add(dto);
                }
            }
        }
    }
    

    Sunday, April 11, 2021 4:19 PM
  • User379720387 posted

    I know I need to do this, but I need to see the mechanics of what I am doing work first.  Otherwise there would be too many leaves up in the air and too many rabit holes down below (like this object mapper).

    There are just 3 coders on this team (me, myself and I), need to keep things simple.

    The DTO separation is one of the many things I still need to do: authentication, Blazor WASM, WebAPI protection, publishing , and a whole of functionality (Quickbooks integration, Google Maps integration, all sorts of listeners, and more.

    Just made a first successful POST.

    Thanks.

    Sunday, April 11, 2021 11:19 PM
  • User1686398519 posted

    Hi wavemaster, 

    object cycle detected error message

    Does your model have circular references?

    If you want to preserve references and handle circular references, you need to set ReferenceHandler to Preserve.

    JsonSerializerOptions options = new()
    {
       ReferenceHandler = ReferenceHandler.Preserve,
       WriteIndented = true
    };
    string tylerJson = JsonSerializer.Serialize(tyler, options);
    
    Employee tylerDeserialized =JsonSerializer.Deserialize<Employee>(tylerJson, options);

    Best Regards,

    YihuiSun

    Monday, April 12, 2021 2:12 AM
  • User379720387 posted

    @YihuiSun

    Note that I have corrected the title to NET5, not 4.5

    My situation is as follows in the Blazor Server page:

    var httpClient = _clientFactory.CreateClient("ServerAPI");
            httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
    
            JsonSerializerOptions options = new()
            {
                ReferenceHandler = ReferenceHandler.Preserve,
                WriteIndented = true
            };
    
            var json = JsonSerializer.Serialize(txnNew); //, options
    
            //var y = JsonSerializer.Deserialize<Transaction>(json, options); //for testing??
    
            var content = new StringContent(json);
    
    
    
            string errorMessage = null;
            content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
            var response = await httpClient.PostAsync($"{baseUrl}/api/Txn/AddNew", content);
    
            var data = await response.Content.ReadAsStringAsync();
            Transaction result = new Transaction();
    
            if (response.IsSuccessStatusCode)

    Note that I have NOT used , options

    My controller POST action:

                    context.Add(txn);
                    await context.SaveChangesAsync();
                    return Ok(txn);

    In this configuration my code completes and a the Transaction record and related records are written in the db

    response informs me about  500 internal server error

    and the line that begins with var data tells meSystem.Json.Text exception and the  possible circular object.

    The json object looks ok to me.

    Then I add , options back in to the line that begins with var json = ....

    Then no records are written to the db

    response days bad request

    data reveals the following error message: 

    {"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"00-e99e67c4d5da7645b6a94b0bca34affd-5e76999d6926874b-00","errors":{"$.PstsNew":["The JSON value could not be converted to System.Collections.Generic.List`1[BtApiEf5.Model.Custom.PstNew]. Path: $.PstsNew | LineNumber: 30 | BytePositionInLine: 14."]}}

    The json it is complaining about is:

    "PstsNew": {
        "$id": "2",
        "$values": [
          {
            "$id": "3",
            "Id": 0,
            "ProviderServiceId": 11,
            "TransactionId": 0,
            "BilledRate": 90.0000
          },
          {
            "$id": "4",
            "Id": 0,
            "ProviderServiceId": 4306,
            "TransactionId": 0,
            "BilledRate": 8.0000
          }
        ]
      },

    And PstNew model looks like this:

    public class PstNew
        {
            public int Id { get; set; }
            public int ProviderServiceId { get; set; }
            public int TransactionId { get; set; }
            public decimal BilledRate { get; set; }
        }

    I am happy that records are written to the db in the no json options scenario. 

    Stared at it a little while this evening and did not find anything suspicious.

    If you see something not right, let me know.

    Monday, April 12, 2021 3:25 AM