Asp.NetCoreApiのModel外部参照でNullを防ぐならIncludeを使え!

この記事は約 7 分で読めます。

みなさんこんにちは!!ひろぽんです!!

最近FirestoreからAsp.Net Core Apiに鞍替えをしまして、色々な学びがあります!

めちゃくちゃ楽しい時期ですね!

今回はそんな学びから一つ!

Asp.Netで外部参照にModelを使っているのに、なぜかNull値が返ってきてエラーが出る。又はデータを取得できない。

といったことになる場合に、Includeを使えば解決できるよーーーということについて書いていきたいと思います!

Asp.NetCoreApiのModel外部参照でNullが出る

まずはこのModel外部参照でNullが出るってどういった状況かということから説明していきます。

namespace IncludeStudy.Models
{
    public class Company
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual ICollection<Employee> Employees { get; set; }

        public Company(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public Company()
        {
            
        }
    }
}

こんな感じでCompanyModelがあってEmployeeModelを複数もっている感じになっています。

EmployeeModelはこんな感じ

namespace IncludeStudy.Models
{
    public class Employee
    {
        [Key]
        public int Id { get; set; }

        public string Name { get; set; }

        [ForeignKey("Company")]
        public virtual int CompanyId { get; set; }
        public virtual Company Company { get; set; }

        public Employee(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public Employee()
        {
            
        }
    }
}

今回使うControllerはCompaniesController.csですので、このControllerの解説だけしておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using IncludeStudy.Models;

namespace IncludeStudy.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CompaniesController : ControllerBase
    {
        private readonly DemoContext _context;

        public CompaniesController(DemoContext context)
        {
            _context = context;
        }

        // GET: api/Companies
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Company>>> GetCompany()
        {
            return await _context.Company.ToListAsync();
        }

        // GET: api/Companies/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Company>> GetCompany(int id)
        {
            var company = await _context.Company.FindAsync(id);

            if (company == null)
            {
                return NotFound();
            }

            return company;
        }

        // GET: api/Companies/5
        [HttpGet("{id}/emp")]
        public async Task<ActionResult<ICollection<Employee>>> GetCompanyEmp(int id)
        {
            var company = await _context.Company.FindAsync(id);

            if (company == null)
            {
                return NotFound();
            }

            return company.Employees.ToList();
        }

        // PUT: api/Companies/5
        // To protect from overposting attacks, enable the specific properties you want to bind to, for
        // more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
        [HttpPut("{id}")]
        public async Task<IActionResult> PutCompany(int id, Company company)
        {
            if (id != company.Id)
            {
                return BadRequest();
            }

            _context.Entry(company).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CompanyExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Companies
        // To protect from overposting attacks, enable the specific properties you want to bind to, for
        // more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
        [HttpPost]
        public async Task<ActionResult<Company>> PostCompany(Company company)
        {
            _context.Company.Add(company);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetCompany", new { id = company.Id }, company);
        }

        // DELETE: api/Companies/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<Company>> DeleteCompany(int id)
        {
            var company = await _context.Company.FindAsync(id);
            if (company == null)
            {
                return NotFound();
            }

            _context.Company.Remove(company);
            await _context.SaveChangesAsync();

            return company;
        }

        private bool CompanyExists(int id)
        {
            return _context.Company.Any(e => e.Id == id);
        }
    }
}

ベースはスキャフォールドで作ったControllerですが、44行目から56行目にハイライトを入れている部分に関してはこちらで作っています。

下記のようなURLを投げればCompanyが持っているEmployeeを返す予定です。

localhost:44389/api/companies/1/emp

ですが実際にビルドしてこのURLをたたいてみると下記のようなエラーが出ます。

要するにValueをNullにできないということ。

ブレイクポイントを仕込んでからデバッグで中身を見てみると下記のようになっています。

せっかくModelで外部参照にしてるのにNullが返されます。

これだとデータが取れないですね。。

Nullが出ないようにするにはIncludeを使う

ここでNullが出てしまう原因はAsp.netでは基本的に遅延ロードをしているからだそうです。

遅延ロードというのは、データを取った段階ではNullだが、Include関数を使えば中に値が入るというもの。

さっきのGetCompanyEmp関数を下記のように変えてみましょう。

        // GET: api/Companies/5
        [HttpGet("{id}/emp")]
        public async Task<ActionResult<ICollection<Employee>>> GetCompanyEmp(int id)
        {
            //var company = await _context.Company.FindAsync(id);

            //if (company == null)
            //{
            //    return NotFound();
            //}

            //return company.Employees.ToList();

            var company = await _context.Company.Include(com => com.Employees).FirstOrDefaultAsync(com => com.Id == id);
            if (company == null)
            {
                return NotFound();
            }

            return company.Employees.ToList();
        }

今回_context.Companyの次にInclude関数を入れています。

この関数はCompanyのEmployeeを読み込んだ状態でデータを出してくれという関数なのです。

なので、これで実行すると下記のようになります。

データが取れていますね!

デバッグで中身を見てみると!

こんな感じで中身が入っている状態でデータが取得できます!

JsonExceptionが出た場合は下記をする。

ただ、この方法を実行すると下記のようなエラーが出ます。

これは直訳すると下記のような意味です。

JsonException:サポートされていない可能性のあるオブジェクトサイクルが検出されました。 これは、サイクルが原因であるか、オブジェクトの深さが最大許容深さ32よりも大きい場合に発生する可能性があります。

つまり、参照の先を参照で取得してそのまた先の参照を取得してって感じで無限ループに入るので、例外が出ているわけです。

こんな状態です。

なのでこれを防ぐために、Nugetで「Microsoft.AspNetCore.Mvc.NewtonsoftJSON」ってのを入れてください。

でStartup.csに下記のように加えてください。

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddDbContext<DemoContext>(opt => opt.UseInMemoryDatabase("includeTest"));
            services.AddControllers().AddNewtonsoftJson(opt =>
                opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);

            var options = new DbContextOptionsBuilder<DemoContext>().UseInMemoryDatabase(databaseName: "includeTest").Options;
            using (var context = new DemoContext(options))
            {
                AddTestData(context);
            }
        }

これでデータが取れるはずです!

お疲れ様でした!

今回のソースコードはGithubで公開しています!

下記リポジトリ解説してる記事よりリポジトリに言っていただき、Aspnetというフォルダ内にあるIncludeStudyというファイルをクローンしてもらえば、今回と同じコードで実験できます!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA