ایجاد یک DbContext مشترک بین entityهای پروژه‌های متفاوت
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

فر ض کنید پروژه بزرگی دارید که هر قسمت را به یک برنامه نویس می‌سپارید تا آن قسمت را در پروژه مجزایی طراحی و برنامه نویسی کند. هر برنامه نویس Entity‌های خاص خود را در لایه‌های مربوط به پروژه خود تعریف می‌کند و از آنها استفاده می‌کند. حال یکی از برنامه نویس‌ها می‌خواهد از Entity های پروژه دیگر استفاده کند. در این صورت اگر از دو Context شیء‌ایی را بسازد و آنها را با یکدیگر Join  بزند، خطایی مربوط به تعلق داشتن دو  Entity به دو Context متفاوت را می‌گیرد.

در پروژه‌های کوچک، کل تیم بر روی ماژول‌های مختلف یک پروژه کار می‌کنند و یک DbContext مشترک دارند. اما راه حل این مشکل در پروژه‌های بزرگ چیست؟ 
یکی از راه‌های پیشنهادی، استفاده از یک کلاس DbContextBase است که همه پروژه‌ها بایستی Context خود را از این کلاس به ارث ببرند که در این صورت باز هم مشکل ساخت چند DbContext وجود خواهد داشت که فقط می‌توان از Entity‌های موجود در DbContextBase و DbContext پروژه جاری استفاده کرد. اما در شرکت‌های بزرگ که پروژه‌هایی مانندERP دارند، روش دیگری استفاده می‌شود که در ادامه خواهیم دید.
روش مورد استفاده به این صورت است که در زمان اجرا یک DbContext برای همه Entity‌های پروژه‌های مختلف ساخته می‌شود. اجازه بدهید همراه با مثال، این پروژه را پیش برویم. فرض کنید دو تیم برنامه نویسی داریم که هر کدام بر روی پروژه‌های مجزای SampleProject1 و SampleProject2 کار میکنند که Entity‌های هر کدام در لایه‌های Common قرار گرفته‌اند.

در SampleProject1 مدل Product را داریم:

public partial class Product : Entity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Nullable<byte> ProductTypeId { get; set; }
    }
و در SampleProject2، مدل ProductType را داریم که هر دو Entity از کلاس Entity ارث بری می‌کند: 
 public partial class ProductType : Entity
    {
        public byte Id { get; set; }
        public string Name { get; set; }
    }
همه پروژه‌ها را در پروژه‌ی SampleProject1.Console، به عنوان رفرنس اضافه می‌کنیم؛ بجز SampleProject2.Console و Output path همه پروژه‌ها را به یک پوشه مشترک هدایت می‌کنیم. در ادامه برای بدست آوردن Entity‌ها از کد زیر استفاده می‌کنیم:
            List<Assembly> allAssemblies = new List<Assembly>();
            string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            foreach (string dll in Directory.GetFiles(path, "*.Common.dll"))
                allAssemblies.Add(Assembly.LoadFile(dll));

            var type = typeof(Entity);
      
            List<Type> types = allAssemblies
             .SelectMany(s => s.GetTypes())
             .Where(p => type.IsAssignableFrom(p)).ToList();

            List<string> entities = new List<string>();
            foreach (var item in types)
            {
                entities.Add(item.Name);
            }

            types.Add(typeof(Entity));
و سپس برای Generate کردن کلاس DbContext از کلاس زیر استفاده می‌کنیم:
public class ContextGenerator
    {
        public void Generate(List<string> entities, params Type[] types)
        {
            StringBuilder code = new StringBuilder();

            code.AppendLine(@"
           using System.Data.Entity;
           using System.Data.Entity.Core.EntityClient;
           using SampleProject1.Common.Models;
           using SampleProject1.Common.Models.Mapping;
           using SampleProject2.Common.Models;
           using SampleProject2.Common.Models.Mapping;

           namespace DbContextGenerator
           {
                public partial class TestContext : DbContext
                {
                    static TestContext()
                    {
                        Database.SetInitializer<TestContext>(null);
                    }

                    public TestContext()
                        : base(""Data Source=.;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True"")
                    {
                        }
                ");

            var pluralizeHelper = new PluralizeHelper();

            foreach (var entity in entities)
            {
                code.AppendLine($@"public DbSet<{entity}> {pluralizeHelper.Pluralize(entity)} {{ get; set; }}");
            }

            code.AppendLine(@"protected override void OnModelCreating(DbModelBuilder modelBuilder)");
            code.AppendLine(@"{");

            foreach (var entity in entities)
            {
                code.AppendLine($@"modelBuilder.Configurations.Add(new {entity}Map());");
            }
            code.AppendLine(@"}");
            code.AppendLine(@"}");
            code.AppendLine(@"}");
           
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters();

            parameters.ReferencedAssemblies.Add("System.Drawing.dll");
            parameters.ReferencedAssemblies.Add("System.Data.dll");
            parameters.ReferencedAssemblies.Add("System.Data.Entity.dll");
            parameters.ReferencedAssemblies.Add("System.ComponentModel.dll");

            foreach (var type in types)
            {
                parameters.ReferencedAssemblies.Add(type.Assembly.Location);
            }

            parameters.ReferencedAssemblies.Add(typeof(DbSet).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(DbContext).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(IQueryable).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(IQueryable<>).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(System.ComponentModel.IListSource).Assembly.Location);

            parameters.GenerateExecutable = false;
            parameters.GenerateInMemory = false;
            parameters.OutputAssembly = "ProjectContext.dll";

            CompilerResults results = provider.CompileAssemblyFromSource(parameters, code.ToString());

            if (results.Errors.HasErrors)
            {
                StringBuilder sb = new StringBuilder();

                foreach (CompilerError error in results.Errors)
                {
                    sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
                }

                throw new InvalidOperationException(sb.ToString());
            }
        }

    }
و نحوه فراخوانی آن:
 new ContextGenerator().Generate(entities, types.ToArray()); // generate dbContext
همانطور که مشاهده می‌کنید، برای تولید کد، از کلاس CSharpCodeProvider استفاده میکنیم که نتیجه اجرای کد بالا، ساخت DLLی به نام ProjectContext.dll است. با مشاهده DLL ساخته شده توسط نرم افزار ILSpy، کد جنریت شده به صورت زیر خواهد بود: 

حال برای استفاده از Context تولید شده، به صورت زیر شیءایی را ساخته:

 static DbContext _dbContext=null;
        public static DbContext GetDbContextInstance()
        {
            if (_dbContext == null)
            {
                string path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
                var dllversionAssm = Assembly.LoadFile(path + "\\ProjectContext.dll");
                Type type = dllversionAssm.GetType("DbContextGenerator.TestContext");
                _dbContext = (DbContext)Activator.CreateInstance(type);
            }
            return _dbContext;
        }

و سپس برای ساخت DbSet از هر Entity به کد زیر نیاز خواهیم داشت:

public static System.Data.Entity.DbSet<T> Get<T>() where T : class
        {
            var set = GetDbContextInstance().Set<T>();
            return set;
        }

هم اکنون می‌توان رکوردهای Entity‌ها را واکشی کرده و یا آن‌ها را با یکدیگر Join بزنیم:

            var products = Get<Product>().ToList();

            var productTypes = Get<ProductType>().ToList();


            var query = from p in Get<Product>()
                        join pt in Get<ProductType>() on p.ProductTypeId equals pt.Id
                        select new
                        {
                            Id = p.Id,
                            Name = p.Name,
                            ProductType = pt.Name

                        };

            var JoinResult = query.ToList();

و نتیجه واکشی ها 


کد کامل این پروژه  

  • #
    ‫۷ سال و ۵ ماه قبل، شنبه ۲ اردیبهشت ۱۳۹۶، ساعت ۱۴:۳۷
    البته یک مشکل اساسی در این روش که وجود دارد و آن امنیت پایین در استفاده از Entity‌های ماژول‌های مختلف است.
    در پروژه‌های بزرگ و ERP هر ماژول باید به یکسری Entityهای مشخصی دسترسی و ارتباط داشته باشد و نباید بصورت نامحدود با هر Entity از هر ماژولی join بزند.
    در این روش تقریبا همه Entityها در یک سطح قرار دارند و کپسوله سازی وجود ندارد.
    • #
      ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۰۰:۴۶
      بله کاملا حق با شماست 
      در واقع این آموزش بیشتر در مورد Generate  کردن Context در زمان اجرا بود اما برای مشکلی که شما اشاره کردید میشه یک قانونی گذاشت که از هر ماژول فقط به متدهایی که در interfaceی که به صفت ComponentServiceInterface مزین شده اند دسترسی داشته باشیم 
          [ComponentServiceInterface]
          public interface ISampleProject1Service
          {
              IQueryable<Product> FetchAllProducts();
          }
      که به زودی در این پروژه پیاده سازیش می‌کنم  
      • #
        ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۰۳:۵۵
        مطلب خوب و مفیدی بود
        سوالی داشتم آیا این روش برای نوشتن Plugin هم میشه استفاده کرد؟ و آیا برای نوشتن Plugin روش بهتری هست که مدل‌ها در داخل خود Plugin باشد؟
        یا روش بهتری را پیشنهاد میکنید؟
        • #
          ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۱۳:۴۶
          روش بهتر ! 
          ترجیح میدم سوالتون و با پروژهای مشابهی که کار کردم جواب بدم 
          چند سال قبل بر روی سیستمی کار می‌کردیم که قرار بود با توجه به درخواست کاربر  دامنه ای رو یک Registerrer خاص ثبت، تمدید، منتقل، حذف و ... کنه 
          چون قرار بود سیستم با امکانات اولیه فروخته بشه  و در صورت خرید پلاگین ها، بقیه امکانات اضافه بشه،
          این قسمت به این صورت کار شده بود که در زمان انجام هر عملی لیست Registerrer ها می‌اومد که هر کدومشون توسط پلاگینشون‌نصب شده بودند و می‌تونستن کارهای ذکر شدرو انجام بدن
          برای پیاده سازی چنین قابلیتی یک interface کلی وجود داشت که وظایف کلی Registerrer ها رو مشخص می‌کرد
          بعد هر پلاگینی که اضافه میشد می‌دونستیم چه کارهایی می‌تونه انجام بده و بعدش با رفلکشن و cast کردن کلاس اصلی هر پلاگین به interface ، به متدهای مورد نظر دسترسی داشتیم . 
          خوب تا اینجا شاید واقعا به یک سیستم pluggable نرسیدیم که شاید بگیم عیب دات نته 
          اجازه بدید یه مثال از php  بزنم 
          یه پروژه  php برای pluggable شدن فقط نیاز داره تا یکسری فایل .php رو در مسیر خاصی در روت اصلی قرار بدیم و ادرسشون و فراخوانی کنیم . کار تمومه و به سیستم  pluggable رسیدیم 
          که برای کامل شدن این مثال بهتره که plugin‌ها به صورت فایل zip نصب بشن که در نهایت همون کپی کردن فایل‌های .php در جاهای مختلف پروژه می‌تونه باشه
          اما تو دات نت یه خورده این کار سخت هست 
          پروژه ای که در حال حاضر کار می‌کنم تقریبا یه سیستم pluggableکامل هست البته از دید من 
          به این صورته که هر برنامه نویسی برای سیستمش سه پروژه 
          UI 
          Common  
          Business  داره 
          به عنوان مثال
          Accounting.UI 
          Accounting.Common  
          Accounting.Business
          و پروژه ای دیگر 
          Store.UI 
          Store.Common  
          Store.Business
          و ....
          که تمام resource‌ها embeded میشن که با build کردن در فایل‌های  dll ذخیره میشن 
          در اجرای اولیه ، کل UI  هایی که resource ی به صورت  embeded دارن لیست میشن و در  زمان درخواست هر صفحه از virtualPathProvider  استفاده می‌کنم تا بتونم اون  resource و از dll مربوطه بخونم و به کاربر نمایش بدم 
          در ادامه هر پروژه میتونه ساختار یکسانی برای تعریف منوهایی داشته باشه که قراره به منوی اصلی اضافه بشن. 
          که در چنین پروژه ای باید یکسری قوانین رو برای ساختار پروژه‌ها برای برنامه نویس‌ها مشخص کنید که طبق اونها پیش برن.
          کار تمومه
          حالا هر چندتا پروژه‌ی مشابه با ساختار مشخص شده به پوشه‌ی  bin  اضافه بشن، میتونن یه plug in باشن که به سیستم اضافه میشن 
  • #
    ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۱۳:۴۸
    «... استفاده از یک کلاس DbContextBase است که همه پروژه‌ها بایستی Context خود را از این کلاس به ارث ببرند که در این صورت باز هم مشکل ساخت چند DbContext وجود خواهد داشت که فقط می‌توان از Entity‌های موجود در DbContextBase و DbContext پروژه جاری استفاده کرد...»
    امکان یافتن DbSetها و همچنین تنظیمات آن‌ها به صورت پویا و سپس افزودن‌شان به Context موجود، میسر هست. کاری که شما در اینجا با تولید کد انجام دادید خلاصه‌ی این دو مطلب هست:
    حالا شاید عنوان کنید که این روش‌ها نمی‌توانند به یک اسمبلی ثالث دسترسی پیدا کنند و محدود هستند به اسمبلی‌های پروژه جاری، اما مورد دوم به صورت رسمی متد modelBuilder.Configurations.AddFromAssembly را دارد و مورد اول متد modelBuilder.RegisterEntityType رسمی و توکار است. متد RegisterEntityType هم نیاز به یک Type را دارد که مثلا از طریق متد Assembly.GetExecutingAssembly().GetTypes و بعد فیلتر کردن Type مدنظر قابل دسترسی هست (برای اسمبلی جاری). برای یافتن یک اسمبلی در کل پروژه جاری از AppDomain.CurrentDomain.GetAssemblies می‌شود شروع کرد و یا متد Assembly.LoadFile امکان بارگذاری یک فایل اسمبلی را می‌دهد.
    نمونه این مفاهیم در پروژه «طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت سوم» استفاده شدن.  
    • #
      ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۱۸:۳۹
      بله این روش هم کاملا درست است . حتما تست می‌کنم . 
      البته از معایب  CSharpCodeProvider میشه به این مقاله اشاره کرد.

       اما بعد از Generate و اجرا به همان Performance  کد‌های Compile شده می‌رسیم 

  • #
    ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۱۸:۲۰
    با تشکر فراوان از مطلب بسیار کاربردی شما.
    اینجا یه سوال مطرح می‌شه . آیا می‌شه از این روش برای  Identity در بین چندین پروژه استفاده کرد ؟ طرح سوال این هست که ما در یک سلوشن چندین پروژه داریم و نیاز هست که تایید و احزار هویت بین تمام پروژه‌ها یکسان باشه .
    آیا می‌شه Identity رو در یک پروژه تعریف کرد و در پروژه‌های همان سلوشن طبق روش شما ازش بهره برد ؟
    تشکر
    • #
      ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۱۸:۴۰
      به این مفهوم single sign-on می‌گن و توسط ASP.NET Identity پشتیبانی نمیشه. برای اینکار باید از Identity server استفاده کنید که برای یک چنین کاری از پایه طراحی شده.
    • #
      ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۱۸:۵۱
      بله 
      فیلتر مورد نظر را در لایه ای مانند Framework که توسط همه پروژه‌ها استفاده می‌شود تعریف کنید 
      public class ClaimBasedAuthorzationAttribute : AuthorizeAttribute
          {
              protected override bool AuthorizeCore(HttpContextBase context)
              {
      
                  // get the user info here [cache is prefered]
      
                  return
                       context.User.Identity is ClaimsIdentity
                      && ((ClaimsIdentity)context.User.Identity).HasClaim(x =>
                          x.Type == "ClaimType" && x.Value.ToLower() == "something".ToLower());
              }
              public override void OnAuthorization(AuthorizationContext filterContext)
              {
                  base.OnAuthorization(filterContext);
              }
          }

      و همه‌ی Action‌های مورد نظر در پروژه‌های مختلف را، به این Attribute مزین کنید
      البته طبق گفته دوستمون "محسن خان" این قابلیت single sign-on نامیده میشه . 
      پاسخ من در در این چارچوب که همه پروژه‌ها تحت نظر یک  framework کار کنن و همه UI‌ها توسط VirtualPathProvider لود بشن میشه