ASP.NET Core Dependency Injection แนวทางปฏิบัติที่ดีที่สุดเคล็ดลับและเทคนิค

ในบทความนี้ฉันจะแบ่งปันประสบการณ์และข้อเสนอแนะของฉันในการใช้การฉีดพึ่งพาในแอปพลิเคชัน ASP.NET Core แรงจูงใจเบื้องหลังหลักการเหล่านี้คือ

  • การออกแบบบริการและการอ้างอิงอย่างมีประสิทธิภาพ
  • การป้องกันปัญหาหลายเธรด
  • การป้องกันการรั่วไหลของหน่วยความจำ
  • การป้องกันข้อบกพร่องที่อาจเกิดขึ้น

บทความนี้อนุมานว่าคุณคุ้นเคยกับ Dependency Injection และ ASP.NET Core ในระดับพื้นฐานอยู่แล้ว มิฉะนั้นโปรดอ่านเอกสารการฉีด ASP.NET Core Dependency ก่อน

ข้อมูลพื้นฐานเกี่ยวกับ

ตัวสร้างการฉีด

Constructor injection ถูกใช้เพื่อประกาศและขอรับการขึ้นต่อกันของบริการในการก่อสร้างบริการ ตัวอย่าง:

ProductService ระดับสาธารณะ
{
    IProductRepository ส่วนตัวแบบอ่านอย่างเดียว _productRepository;
    สาธารณะ ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    โมฆะสาธารณะลบ (int id)
    {
        _productRepository.Delete (ID);
    }
}

ProductService กำลังฉีด IProductRepository เป็นการอ้างอิงในตัวสร้างของมันแล้วใช้มันภายในวิธีการลบ

แนวทางปฏิบัติที่ดี:

  • กำหนดการอ้างอิงที่ต้องการอย่างชัดเจนในตัวสร้างบริการ ดังนั้นจึงไม่สามารถสร้างบริการโดยไม่ต้องพึ่งพา
  • กำหนดการพึ่งพาการฉีดให้กับฟิลด์ / คุณสมบัติแบบอ่านอย่างเดียว (เพื่อป้องกันการกำหนดค่าอื่นโดยไม่ตั้งใจภายในเมธอด)

ฉีดคุณสมบัติ

คอนเทนเนอร์การฉีดตามมาตรฐานของ ASP.NET Core ไม่สนับสนุนการฉีดคุณสมบัติ แต่คุณสามารถใช้คอนเทนเนอร์อื่นที่สนับสนุนคุณสมบัติการฉีด ตัวอย่าง:

ใช้ Microsoft.Extensions.Logging;
ใช้ Microsoft.Extensions.Logging.Abstractions;
namespace MyApp
{
    ProductService ระดับสาธารณะ
    {
        ILogger สาธารณะ  คนตัดไม้ {รับ; ตั้ง; }
        IProductRepository ส่วนตัวแบบอ่านอย่างเดียว _productRepository;
        สาธารณะ ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        โมฆะสาธารณะลบ (int id)
        {
            _productRepository.Delete (ID);
            Logger.LogInformation (
                $ "ลบผลิตภัณฑ์ที่มี id = {id}");
        }
    }
}

ProductService กำลังประกาศคุณสมบัติ Logger ด้วย setter สาธารณะ คอนเทนเนอร์ฉีดพึ่งพาสามารถตั้งค่า Logger ถ้ามันมีอยู่ (ลงทะเบียนไปยังภาชนะ DI ก่อน)

แนวทางปฏิบัติที่ดี:

  • ใช้การฉีดคุณสมบัติเฉพาะสำหรับการอ้างอิงเพิ่มเติม นั่นหมายความว่าบริการของคุณสามารถทำงานได้อย่างถูกต้องหากไม่มีการอ้างอิงเหล่านี้
  • ใช้รูปแบบวัตถุ Null (เช่นเดียวกับในตัวอย่างนี้) ถ้าเป็นไปได้ มิฉะนั้นให้ตรวจสอบค่าว่างเสมอขณะใช้การอ้างอิง

ผู้ให้บริการ

รูปแบบตัวระบุตำแหน่งบริการเป็นอีกวิธีหนึ่งในการรับการอ้างอิง ตัวอย่าง:

ProductService ระดับสาธารณะ
{
    IProductRepository ส่วนตัวแบบอ่านอย่างเดียว _productRepository;
    ส่วนตัวอ่านอย่างเดียว ILogger  _logger;
    ผลิตภัณฑ์สาธารณะบริการ (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    โมฆะสาธารณะลบ (int id)
    {
        _productRepository.Delete (ID);
        _logger.LogInformation ($ "ลบผลิตภัณฑ์ที่มี id = {id}");
    }
}

ProductService กำลังฉีด IServiceProvider และแก้ไขการอ้างอิงโดยใช้ GetRequiredService ส่งข้อยกเว้นถ้าการอ้างอิงที่ร้องขอไม่ได้ลงทะเบียนก่อน ในทางกลับกัน GetService จะส่งคืนค่าว่างในกรณีนั้น

เมื่อคุณแก้ไขบริการภายในตัวสร้างพวกเขาจะนำออกใช้เมื่อมีการให้บริการ ดังนั้นคุณไม่ต้องกังวลเกี่ยวกับการปล่อย / กำจัดการบริการที่แก้ไขภายใน Constructor (เช่น Constructor และการฉีดคุณสมบัติ)

แนวทางปฏิบัติที่ดี:

  • อย่าใช้รูปแบบตัวระบุตำแหน่งของบริการหากเป็นไปได้ (ถ้าทราบชนิดของบริการในเวลาในการพัฒนา) เพราะมันทำให้การพึ่งพาโดยนัย ซึ่งหมายความว่าไม่สามารถดูการพึ่งพาได้ง่ายในขณะที่สร้างอินสแตนซ์ของบริการ สิ่งนี้มีความสำคัญอย่างยิ่งสำหรับการทดสอบหน่วยที่คุณอาจต้องการจำลองการขึ้นต่อกันของบริการ
  • แก้ไขการอ้างอิงในตัวสร้างบริการถ้าเป็นไปได้ การแก้ไขในวิธีการบริการทำให้แอปพลิเคชันของคุณซับซ้อนและผิดพลาดมากขึ้น ฉันจะกล่าวถึงปัญหา & วิธีแก้ไขในหัวข้อถัดไป

อายุการใช้งาน

อายุการใช้งานของบริการมีสามช่วงเวลาในการฉีด ASP.NET Core Dependency:

  1. บริการชั่วคราวถูกสร้างขึ้นทุกครั้งที่มีการฉีดหรือร้องขอ
  2. บริการที่กำหนดขอบเขตถูกสร้างขึ้นตามขอบเขต ในเว็บแอปพลิเคชันทุกคำขอของเว็บจะสร้างขอบเขตบริการใหม่แยกออกจากกัน นั่นหมายถึงบริการที่กำหนดขอบเขตโดยทั่วไปจะถูกสร้างขึ้นตามคำขอของเว็บ
  3. บริการ Singleton ถูกสร้างขึ้นต่อคอนเทนเนอร์ DI ซึ่งโดยทั่วไปหมายความว่าพวกเขาสร้างเพียงครั้งเดียวต่อแอปพลิเคชันและใช้ตลอดอายุการใช้งานของแอปพลิเคชัน

ภาชนะ DI ติดตามบริการที่ได้รับการแก้ไขทั้งหมด บริการต่างๆจะถูกปล่อยออกมาและใช้งานเมื่อสิ้นสุดอายุการใช้งาน

  • หากบริการมีการอ้างอิงพวกเขาจะถูกปล่อยออกมาและจำหน่ายโดยอัตโนมัติ
  • หากบริการใช้ส่วนต่อประสาน IDisposable วิธีการกำจัดจะถูกเรียกโดยอัตโนมัติในการเปิดตัวบริการ

แนวทางปฏิบัติที่ดี:

  • ลงทะเบียนบริการของคุณเป็นแบบชั่วคราวทุกที่ที่ทำได้ เพราะง่ายต่อการออกแบบบริการชั่วคราว โดยทั่วไปคุณไม่สนใจเกี่ยวกับการรั่วไหลของหลายเธรดและหน่วยความจำและคุณรู้ว่าบริการนั้นมีอายุการใช้งานสั้น
  • ใช้อายุการใช้งานที่ จำกัด ของบริการอย่างระมัดระวังเนื่องจากอาจยุ่งยากหากคุณสร้างขอบเขตบริการย่อยหรือใช้บริการเหล่านี้จากแอปพลิเคชันที่ไม่ใช่เว็บ
  • ใช้อายุการใช้งานเดี่ยวอย่างระมัดระวังตั้งแต่นั้นคุณต้องจัดการกับปัญหาหลายเธรดและหน่วยความจำรั่วที่อาจเกิดขึ้น
  • อย่าขึ้นอยู่กับบริการชั่วคราวหรือบริการที่กำหนดขอบเขตจากบริการซิงเกิล เนื่องจากบริการชั่วคราวกลายเป็นอินสแตนซ์ซิงเกิลเมื่อเซอร์วิสซิงเกิลอัดฉีดและอาจทำให้เกิดปัญหาได้หากบริการชั่วคราวไม่ได้ออกแบบมาเพื่อรองรับสถานการณ์ดังกล่าว ที่เก็บ DI เริ่มต้นของ ASP.NET Core แล้วโยนข้อยกเว้นในกรณีเช่นนี้

การแก้ปัญหาบริการในร่างกายวิธี

ในบางกรณีคุณอาจต้องแก้ไขบริการอื่นในวิธีการบริการของคุณ ในกรณีเช่นนี้ให้แน่ใจว่าคุณปล่อยบริการหลังการใช้งาน วิธีที่ดีที่สุดในการสร้างความมั่นใจว่าจะสร้างขอบเขตบริการ ตัวอย่าง:

PriceCalculator ระดับสาธารณะ
{
    IServiceProvider ส่วนตัวแบบอ่านอย่างเดียว _serviceProvider;
    เครื่องคิดเลขราคาสาธารณะ (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    คำนวณค่าลอยตัวสาธารณะ (ผลิตภัณฑ์ผลิตภัณฑ์จำนวนนับ
      พิมพ์ taxStrategyServiceType)
    {
        using (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            ราคา var = สินค้าราคา * นับ;
            ราคาส่งคืน + taxStrategy.CalculateTax (ราคา);
        }
    }
}

PriceCalculator อัดฉีด IServiceProvider ในคอนสตรัคเตอร์และกำหนดให้กับฟิลด์ PriceCalculator จากนั้นใช้ภายในวิธีการคำนวณเพื่อสร้างขอบเขตบริการย่อย มันใช้ scope.ServiceProvider เพื่อแก้ไขบริการแทนอินสแตนซ์ _serviceProvider ที่ถูกแทรก ดังนั้นบริการทั้งหมดที่ได้รับการแก้ไขจากขอบเขตจะเผยแพร่ / จำหน่ายโดยอัตโนมัติเมื่อสิ้นสุดคำสั่งการใช้งาน

แนวทางปฏิบัติที่ดี:

  • หากคุณกำลังแก้ไขบริการในเมธอดเมธอดให้สร้างขอบเขตเซอร์วิสย่อยเสมอเพื่อให้แน่ใจว่าเซอร์วิสที่ได้รับการแก้ไขนั้นถูกปล่อยออกมาอย่างถูกต้อง
  • หากเมธอดรับ IServiceProvider เป็นอาร์กิวเมนต์คุณสามารถแก้ไขบริการได้โดยตรงโดยไม่ต้องกังวลเกี่ยวกับการปล่อย / กำจัด การสร้าง / จัดการขอบเขตบริการเป็นความรับผิดชอบของรหัสที่เรียกวิธีการของคุณ การปฏิบัติตามหลักการนี้ทำให้โค้ดของคุณสะอาดขึ้น
  • อย่าถือการอ้างอิงถึงบริการที่ได้รับการแก้ไข! มิฉะนั้นอาจทำให้เกิดการรั่วไหลของหน่วยความจำและคุณจะสามารถเข้าถึงบริการที่จำหน่ายเมื่อคุณใช้การอ้างอิงวัตถุในภายหลัง (เว้นแต่ว่าบริการที่ได้รับการแก้ไขเป็นแบบซิงเกิล)

บริการซิงเกิล

บริการซิงเกิลได้รับการออกแบบโดยทั่วไปเพื่อให้สถานะของแอปพลิเคชัน แคชเป็นตัวอย่างที่ดีของสถานะแอปพลิเคชัน ตัวอย่าง:

FileService ระดับสาธารณะ
{
    ส่วนตัวอ่านอย่างเดียว ConcurrentDictionary  _cache;
    FileService สาธารณะ ()
    {
        _cache = new ConcurrentDictionary  ();
    }
    ไบต์สาธารณะ [] GetFileContent (สตริง filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            กลับ File.ReadAllBytes (filePath);
        });
    }
}

FileService เพียงแคชเนื้อหาของไฟล์เพื่อลดการอ่านดิสก์ บริการนี้ควรลงทะเบียนเป็นซิงเกิล มิฉะนั้นการแคชจะไม่ทำงานอย่างที่คาดไว้

แนวทางปฏิบัติที่ดี:

  • ถ้าบริการมีสถานะอยู่ก็ควรเข้าถึงสถานะนั้นในลักษณะที่ปลอดภัยของเธรด เพราะคำขอทั้งหมดใช้อินสแตนซ์ของบริการเดียวกันพร้อมกัน ฉันใช้ ConcurrentDictionary แทน Dictionary เพื่อความปลอดภัยของเธรด
  • อย่าใช้บริการที่กำหนดขอบเขตหรือชั่วคราวจากบริการซิงเกิล เนื่องจากบริการชั่วคราวอาจไม่ได้รับการออกแบบให้ปลอดภัยต่อเธรด หากคุณต้องใช้พวกเขาให้ดูแลมัลติเธรดในขณะที่ใช้บริการเหล่านี้ (ใช้ล็อกเป็นต้น)
  • การรั่วไหลของหน่วยความจำมักเกิดจากบริการซิงเกิล พวกเขาจะไม่ปล่อย / จำหน่ายจนกว่าจะสิ้นสุดของใบสมัคร ดังนั้นหากพวกเขายกตัวอย่างคลาส (หรือฉีด) แต่ไม่ปล่อย / กำจัดพวกเขาก็จะยังคงอยู่ในหน่วยความจำจนกว่าจะสิ้นสุดของแอพลิเคชัน ตรวจสอบให้แน่ใจว่าคุณปล่อย / ทิ้งในเวลาที่เหมาะสม ดูส่วนบริการแก้ไขในส่วนวิธีการด้านบน
  • หากคุณแคชข้อมูล (เนื้อหาไฟล์ในตัวอย่างนี้) คุณควรสร้างกลไกเพื่ออัปเดต / ทำให้ข้อมูลแคชใช้ไม่ได้เมื่อแหล่งข้อมูลดั้งเดิมเปลี่ยนไป (เมื่อไฟล์แคชเปลี่ยนแปลงบนดิสก์สำหรับตัวอย่างนี้)

บริการที่กำหนดขอบเขต

อายุการใช้งานที่กำหนดไว้ก่อนดูเหมือนจะเป็นตัวเลือกที่ดีในการจัดเก็บต่อข้อมูลคำขอทางเว็บ เนื่องจาก ASP.NET Core สร้างขอบเขตบริการตามคำขอของเว็บ ดังนั้นหากคุณลงทะเบียนบริการเป็นระยะ ๆ สามารถแบ่งปันได้ระหว่างการร้องขอทางเว็บ ตัวอย่าง:

RequestItemsService ระดับสาธารณะ
{
    พจนานุกรมส่วนตัว <อ่านอย่างเดียวสตริง> _items;
    สาธารณะ RequestItemsService ()
    {
        _items = พจนานุกรมใหม่  ();
    }
    โมฆะสาธารณะ Set (ชื่อสตริงค่าวัตถุ)
    {
        _items [name] = value;
    }
    วัตถุสาธารณะรับ (ชื่อสตริง)
    {
        return _items [ชื่อ];
    }
}

หากคุณลงทะเบียน RequestItemsService เป็นการกำหนดขอบเขตและแทรกลงในบริการที่แตกต่างกันสองรายการคุณสามารถรับไอเท็มที่เพิ่มจากบริการอื่นได้เนื่องจากพวกเขาจะแชร์อินสแตนซ์ RequestItemsService เดียวกัน นั่นคือสิ่งที่เราคาดหวังจากบริการที่กำหนดขอบเขต

แต่ .. ความจริงอาจไม่เป็นเช่นนั้นเสมอไป หากคุณสร้างขอบเขตบริการย่อยและแก้ไข RequestItemsService จากขอบเขตย่อยคุณจะได้รับอินสแตนซ์ใหม่ของ RequestItemsService และจะไม่ทำงานตามที่คุณคาดหวัง ดังนั้นบริการที่กำหนดขอบเขตไม่ได้หมายถึงอินสแตนซ์ต่อคำขอของเว็บเสมอไป

คุณอาจคิดว่าคุณไม่ได้ทำผิดพลาดอย่างเห็นได้ชัด (แก้ไขขอบเขตภายในขอบเขตลูก) แต่นี่ไม่ใช่ข้อผิดพลาด (การใช้งานปกติมาก) และกรณีอาจไม่ง่ายอย่างนั้น หากมีกราฟการพึ่งพาขนาดใหญ่ระหว่างบริการของคุณคุณไม่สามารถรู้ได้ว่าใครสร้างขอบเขตลูกและแก้ไขบริการที่ฉีดบริการอื่น ... ซึ่งในที่สุดจะแทรกบริการที่มีขอบเขต

แนวปฏิบัติที่ดี:

  • บริการที่ถูกกำหนดขอบเขตอาจถือได้ว่าเป็นการปรับให้เหมาะสมซึ่งบริการดังกล่าวถูกส่งเข้ามาด้วยการร้องขอบริการทางเว็บ ดังนั้นบริการทั้งหมดนี้จะใช้อินสแตนซ์เดียวของบริการระหว่างคำขอเว็บเดียวกัน
  • บริการที่กำหนดขอบเขตไม่จำเป็นต้องออกแบบให้ปลอดภัยสำหรับเธรด เนื่องจากโดยปกติแล้วพวกเขาควรจะใช้ web-request / thread เดียว แต่…ในกรณีนี้คุณไม่ควรแชร์ขอบเขตการบริการระหว่างเธรดที่แตกต่างกัน!
  • ระวังหากคุณออกแบบบริการที่กำหนดขอบเขตเพื่อใช้ข้อมูลร่วมกันระหว่างบริการอื่น ๆ ในคำขอทางเว็บ (อธิบายด้านบน) คุณสามารถจัดเก็บข้อมูลคำขอทางเว็บไว้ใน HttpContext (ฉีด IHttpContextAccessor เพื่อเข้าถึง) ซึ่งเป็นวิธีที่ปลอดภัยกว่าในการทำเช่นนั้น อายุการใช้งานของ HttpContext ไม่ถูกกำหนดขอบเขต ที่จริงแล้วมันไม่ได้ลงทะเบียนกับ DI เลย (นั่นคือเหตุผลที่คุณไม่ต้องฉีด แต่ให้ฉีด IHttpContextAccessor แทน) การประยุกต์ใช้ HttpContextAccessor ใช้ AsyncLocal เพื่อแบ่งปัน HttpContext เดียวกันระหว่างการร้องขอเว็บ

ข้อสรุป

การฉีดการพึ่งพานั้นดูง่ายต่อการใช้งานในตอนแรก แต่อาจมีปัญหาหลายเธรดและการรั่วไหลของหน่วยความจำหากคุณไม่ปฏิบัติตามหลักการที่เข้มงวด ฉันแบ่งปันหลักการที่ดีบางอย่างจากประสบการณ์ของตัวเองในระหว่างการพัฒนากรอบ ASP.NET Boilerplate