[HttpGet] public IAsyncEnumerable<Product> Get() => productsRepository.GetAllProducts();
[ComponentServiceInterface] public interface ISampleProject1Service { IQueryable<Product> FetchAllProducts(); }
نکته: آشنایی با مفاهیم MEF برای درک بهتر مطالب الزامی است.
*در صورتی که با مفاهیم MEF آشنایی ندارید میتوانید از اینجا شروع کنید.
در MEF سه نوع PartCreationPolicy وجود دارد:
#1 Shared : آبجکت مورد نظر فقط یک بار در کل طول عمر Composition Container ساخته میشود.(Singleton)
#2 NonShared : آبجکت مورد نظر به ازای هر درخواست دوباره نمونه سازی میشود.
#3 Any : از حالت پیش فرض CompositionContainar برای نمونه سازی استفاده میشود که همان مورد اول است(Shared)
در اکثر پروژهها ساخت نمونه اشیا به صورت Singleton میسر نیست و باعث اشکال در پروژه میشود. برای حل این مشکل باید PartCreationPolicy رو برای هر شی مجزا تعریف کنیم. برای مثال
[Export] [PartCreationPolicy( CreationPolicy.NonShared )] internal class ShellViewModel : ViewModel<IShellView> { private readonly DelegateCommand exitCommand; [ImportingConstructor] public ShellViewModel( IShellView view ) : base( view ) { exitCommand = new DelegateCommand( Close ); } }
برای حل این مسئله بهتر است که رفتار Composition Container رو در هنگام نمونه سازی تغییر دهیم. یعنی آبجکتها به صورت پیش فرض به صورت NonShared تولید شوند و در صورت نیاز به نمونه Shared این Attribute رو در کلاس مورد نظر استفاده کنیم. کافیست از کلاس Composition Container که قلب MEF محسوب میشود ارث برده و رفتار مورد نظر را Override کنیم. برای نمونه :
public class CustomCompositionContainer : CompositionContainer { public CustomCompositionContainer(ComposablePartCatalog catalog) : base(catalog) { } protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition) { definition = AdaptDefinition(definition); return base.GetExportsCore(definition); } private ImportDefinition AdaptDefinition(ImportDefinition definition) { ContractBasedImportDefinition namedDefinition = definition as ContractBasedImportDefinition; if (namedDefinition != null && namedDefinition.RequiredCreationPolicy == CreationPolicy.Any) { definition = new ContractBasedImportDefinition(namedDefinition.ContractName, namedDefinition.RequiredMetadata, namedDefinition.Cardinality, namedDefinition.IsRecomposable, namedDefinition.IsPrerequisite, CreationPolicy.NonShared); } return definition; } }
حال فقط کافیست در پروژه به جای استفاده از CompositionContainer از CustomCompositionContainer استفاده کنیم.
EF Code First #12
- IList هم دقیقا از IEnumerable مشتق شده و یک سری قابلیت مانند افزودن آیتم به آن و همچنین دسترسی بر اساس ایندکس به آن اضافه شده.
- اگر در حین کار با خروجی لیستی یک متد، فراخوانیهای بعدی نتیجه آن، فقط یکبار است، از IEnumerable استفاده کنید. اگر بیشتر از یکبار است از IList استفاده کنید. چون در EF هربار مراجعه به نتیجه یک IEnumerable مساوی است با واکشی دوباره اطلاعات از سرور. در حالت استفاده از IList کار یکبار انجام شده و تمام میشود.
نام این جدول را با درنظر گرفتن شرایط موجود میتوان Resources گذاشت.
ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.
ستون Key برای نگهداری کلید ورودی منبع استفاده میشود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره میشود.
ستون Culture برای ذخیره کالچر ورودی منبع به کار میرود. این مقدار میتواند برای کالچر پیشفرض برنامه برابر رشته خالی باشد.
ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده میشود.
برای ستون Id میتوان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.
نکته: برای امنیت بیشتر میتوان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.
برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش میدهد دقت کنید:
اصلاح کلاس DbResourceProviderFactory
برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید بهصورت زیر تغییر کند:
public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { if (!string.IsNullOrEmpty(virtualPath)) { virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/' } return new LocalDbResourceProvider(virtualPath); }
ارتباط با دیتابیس
خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راههای زیادی وجود دارد. برای پیادهسازی آن مثلا میتوان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا بهکارگیری IoC، نمونه مناسبی از پیادهسازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیادهسازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.
پس از پیادهسازی کلاسهای مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با دادهها استفاده میشود که نمونهای ابتدایی از آن در زیر آورده شده است:
using System.Collections.Generic; using System.Linq; using DbResourceProvider.Models; namespace DbResourceProvider.Data { public class ResourceData { private readonly string _resourceName; public ResourceData(string resourceName) { _resourceName = resourceName; } public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList(); } } } }
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { private readonly string _resourceName; private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture; public DbResourceManager(string resourceName) { _resourceName = resourceName; _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>(); } public object GetObject(string resourceKey, CultureInfo culture) { return GetCachedObject(resourceKey, culture.Name); } private object GetCachedObject(string resourceKey, string cultureName) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResource = _resourceCacheByCulture[cultureName]; lock (this) { if (!cachedResource.ContainsKey(resourceKey)) { var data = new ResourceData(_resourceName); var dbResource = data.GetResource(resourceKey, cultureName); if (dbResource == null) return null; var cachedResources = _resourceCacheByCulture[cultureName]; cachedResources.Add(dbResource.Key, dbResource.Value); } } return cachedResource[resourceKey]; } } }
private object GetCachedObject(string resourceKey, string cultureName) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) { _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResources = _resourceCacheByCulture[cultureName]; var data = new ResourceData(_resourceName); var dbResources = data.GetResources(cultureName); foreach (var dbResource in dbResources) { cachedResources.Add(dbResource.Key, dbResource.Value); } } } var cachedResource = _resourceCacheByCulture[cultureName]; return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey]; }
using System.Collections; using System.Collections.Generic; using System.Globalization; namespace DbResourceProvider { public class CultureFallbackProvider : IEnumerable<CultureInfo> { private readonly CultureInfo _startingCulture; private readonly CultureInfo _neutralCulture; private readonly bool _tryParentCulture; public CultureFallbackProvider(CultureInfo startingCulture = null, CultureInfo neutralCulture = null, bool tryParentCulture = true) { _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture; _neutralCulture = neutralCulture; _tryParentCulture = tryParentCulture; } #region Implementation of IEnumerable<CultureInfo> public IEnumerator<CultureInfo> GetEnumerator() { var reachedNeutralCulture = false; var currentCulture = _startingCulture; do { if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name) { yield return CultureInfo.InvariantCulture; reachedNeutralCulture = true; break; } yield return currentCulture; currentCulture = currentCulture.Parent; } while (_tryParentCulture && !HasInvariantCultureName(currentCulture)); if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture) yield break; yield return CultureInfo.InvariantCulture; } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion private bool HasInvariantCultureName(CultureInfo culture) { return culture.Name == CultureInfo.InvariantCulture.Name; } } }
public object GetObject(string resourceKey, CultureInfo culture) { foreach (var currentCulture in new CultureFallbackProvider(culture)) { var value = GetCachedObject(resourceKey, currentCulture.Name); if (value != null) return value; } throw new KeyNotFoundException("The specified 'resourceKey' not found."); }
ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.
دادههای نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:
<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" /> <asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):
<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
میبینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.
کار تولید یک پرووایدر منابع سفارشی دیتابیسی به اتمام رسید. تا اینجا اصول کلی تولید یک پرووایدر سفارشی شرح داده شد. بدین ترتیب میتوان برای هر حالت خاص دیگری نیز پرووایدرهای سفارشی مخصوص ساخت تا مدیریت منابع به آسانی تحت کنترل برنامه نویس قرار گیرد.
اما نکتهای را که باید به آن توجه کنید این است که در پیادهسازیهای نشان داده شده با توجه به نحوه کششدن مقادیر ورودیها، اگر این مقادیر در دیتابیس تغییر کنند، تا زمانیکه سایت ریست نشود این تغییرات در برنامه اعمال نخواهد شد. زیرا همانطور که اشاره شد، مدیریت نمونههای تولیدشده از پرووایدرهای منابع برای هر منبع درخواستی درنهایت برعهده ASP.NET است. بنابراین باید مکانیزمی پیاده شود تا کلاس DbResourceManager از بهروزرسانی ورودیهای کششده اطلاع یابد تا آنها را ریفرش کند.
در ادامه درباره روشهای مختلف نحوه پیادهسازی قابلیت بهروزرسانی ورودیهای منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راهحلهای مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژههای MVC شرح داده میشود.
البته مباحث پیشرفتهتری چون تزریق وابستگی برای پیادهسازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.
منابع
http://msdn.microsoft.com/en-us/library/aa905797.aspx
http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx
http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs
http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
// In ParseClaimsFromJwt method return new JwtInfo { Claims = claims, Roles = roles, ExpirationDateUtc = expirationDateUtc, IsExpired = isExpired }; // In GetAuthenticationStateAsync with cannot convert error return new AuthenticationState( new ClaimsPrincipal( new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType") ) ); // Resharper suggestion: Explicit cast but with suspicious cast warning. return new AuthenticationState( new ClaimsPrincipal( new ClaimsIdentity((IEnumerable<Claim>)JwtParser.ParseClaimsFromJwt(token), "jwtAuthType") ) );
کوئری هایی با قابلیت استفاده ی مجدد
- برای پویا کردن LINQ امکان استفاده از رشتهها وجود داره: (^)
- نوع خروجی متد در این حالت خاص میتونه object یا IEnumerable خالی باشد.
اگه این پست رو مطالعه کرده باشید یه مشکل امنیتی بنام «Mass Assignment» مطرح شد.برای رفع این مشکل یک روش استفاده از ViewModel بود.
فرض کنید Model ما
public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string UserName { get; set; } public string Password { get; set; } public bool IsAdmin { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } }
public class UserViewModel { public string FirstName { get; set; } public string LastName { get; set; } public string Password { get; set; } }
برای استفاده UserViewModel بعنوان Model در View ی مورد نظر باید شی UserViewModel رو با اطلاعات شی User مقدار دهی کنیم مثلا با کدی مثل این در کنترلر.
public ActionResult Index(int id = 1) { var user = _userService.GetById(id); var userViewModel = new UserViewModel { FirstName = user.FirstName, LastName = user.LastName, Password = user.Password }; return View(userViewModel); }
برای استفاده از AutoMapper از نوگت استفاده میکنیم.
PM> Install-Package AutoMapper
public static class AutoMapperWebConfiguration { public static void Configure() { ConfigureUserMapping(); } private static void ConfigureUserMapping() { Mapper.CreateMap<User, UserViewModel>(); } }
اولین پارامتر نوع مبدا و دومین پارامتر نوع مقصد هست.
برای انجام نگاشت هم از متد Map استفاده میکنیم.
public ActionResult Index(int id=1) { var user = _userService.GetById(id); var userViewModel=new UserViewModel(); AutoMapper.Mapper.Map(user, userViewModel); return View(userViewModel); }
ادامه دارد...
نگاهی به محتوای JSON Web Token تولیدی
اگر مطلب قسمت قبل را پیگیری کرده باشید، پس از لاگین، یک چنین خروجی را در کنسول توسعه دهندگان مرورگر میتوان مشاهده کرد که همان return Ok(new { access_token = jwt }) دریافتی از سمت سرور است:
اکنون این رشتهی طولانی را در حافظه کپی کرده و سپس به سایت https://jwt.io/#debugger-io مراجعه و در قسمت دیباگر آن، این رشتهی طولانی را paste میکنیم تا آنرا decode کند:
برای نمونه payload آن حاوی یک چنین اطلاعاتی است:
{ "jti": "b2921057-32a4-fbb2-0c18-5889c1ab8e70", "iss": "https://localhost:5001/", "iat": 1576402824, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid N.", "DisplayName": "Vahid N.", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1576402824, "exp": 1576402944, "aud": "Any" }
استخراج اطلاعات کاربر وارد شدهی به سیستم، از JSON Web Token دریافتی
همانطور که در payload توکن دریافتی از سرور نیز مشخص است، اطلاعات ارزشمندی از کاربر، به همراه آن ارائه شدهاند و مزیت کار با آن، عدم نیاز به کوئری گرفتن مداوم از سرور و بانک اطلاعاتی، جهت دریافت مجدد این اطلاعات است. بنابراین اکنون در برنامهی React خود، قصد داریم مشابه کاری را که سایت jwt.io انجام میدهد، پیاده سازی کرده و به این اطلاعات دسترسی پیدا کنیم و برای مثال DisplayName را در Navbar نمایش دهیم. برای این منظور فایل app.js را گشوده و تغییرات زیر را به آن اعمال میکنیم:
- میخواهیم اطلاعات کاربر جاری را در state کامپوننت مرکزی App قرار دهیم. سپس زمانیکه کار رندر کامپوننت NavBar درج شدهی در متد رندر آن فرا میرسد، میتوان این اطلاعات کاربر را به صورت props به آن ارسال کرد؛ و یا به هر کامپوننت دیگری در component tree برنامه.
- بنابراین ابتدا کامپوننت تابعی بدون حالت App را تبدیل به یک کلاس کامپوننت استاندارد مشتق شدهی از کلاس پایهی Component میکنیم. اکنون میتوان state را نیز به آن اضافه کرد:
class App extends Component { state = {};
- برای decode کردن توکن، نیاز به نصب کتابخانهی زیر را داریم:
> npm install --save jwt-decode
import jwtDecode from "jwt-decode"; // ... class App extends Component { state = {}; componentDidMount() { try { const jwt = localStorage.getItem("token"); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); this.setState({ currentUser }); } catch (ex) { console.log(ex); } }
- اکنون میتوان شیء currentUser را به صورت props، به کامپوننت NavBar ارسال کرد:
render() { return ( <React.Fragment> <ToastContainer /> <NavBar user={this.state.currentUser} /> <main className="container">
نمایش اطلاعات کاربر وارد شدهی به سیستم در NavBar
پس از ارسال شیء کاربر به صورت props به کامپوننت src\components\navBar.jsx، کدهای این کامپوننت را به صورت زیر جهت نمایش نام کاربر جاری وارد شدهی به سیستم تغییر میدهیم:
const NavBar = ({ user }) => {
سپس میتوان لینکهای Login و Register را به صورت شرطی رندر کرد و نمایش داد:
{!user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/login"> Login </NavLink> <NavLink className="nav-item nav-link" to="/register"> Register </NavLink> </React.Fragment> )}
شبیه به همین حالت را برای هنگامیکه کاربر، تعریف شدهاست، جهت نمایش نام او و لینک به Logout، نیاز داریم:
{user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink> <NavLink className="nav-item nav-link" to="/profile"> {user.DisplayName} </NavLink> </React.Fragment> )}
فعلا تا پیش از پیاده سازی Logout، برای آزمایش آن، به کنسول توسعه دهندگان مرورگر مراجعه کرده و توکن ذخیره شدهی در ذیل قسمت application->storage را دستی حذف کنید. سپس صفحه را ریفرش کنید. اینبار لینکهای به Login و Register نمایان میشوند.
یک مشکل! در این حالت (زمانیکه توکن حذف شدهاست)، از طریق قسمت Login به برنامه وارد شوید. هرچند این قسمتها به درستی کار خود را انجام میدهند، اما هنوز در منوی بالای سایت، نام کاربری و لینک به Logout ظاهر نشدهاند. علت اینجا است که در کامپوننت App، کار دریافت توکن در متد componentDidMount انجام میشود و این متد نیز تنها یکبار در طول عمر برنامه فراخوانی میشود. برای رفع این مشکل به src\components\loginForm.jsx مراجعه کرده و بجای استفاده از history.push برای هدایت کاربر به صفحهی اصلی برنامه، نیاز خواهیم داشت تا کل برنامه را بارگذاری مجدد کنیم. یعنی بجای:
this.props.history.push("/");
window.location = "/";
پیاده سازی Logout کاربر وارد شدهی به سیستم
برای logout کاربر تنها کافی است توکن او را از local storage حذف کنیم. به همین جهت مسیریابی جدید logout را که به صورت لینکی به NavBar اضافه کردیم:
<NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink>
import Logout from "./components/logout"; // ... class App extends Component { render() { return ( // ... <Switch> // ... <Route path="/logout" component={Logout} />
import { Component } from "react"; class Logout extends Component { componentDidMount() { localStorage.removeItem("token"); window.location = "/"; } render() { return null; } } export default Logout;
بهبود کیفیت کدهای نوشته شده
اگر به کامپوننت App دقت کنید، کلید token استفاده شدهی در آن، در چندین قسمت برنامه مانند login و logout، تکرار و پراکنده شدهاست. بنابراین بهتر است جزئیات پیاده سازی مرتبط با اعتبارسنجی کاربران، به ماژول مختص به آنها (src\services\authService.js) منتقل شود تا سایر قسمتهای برنامه، به صورت یکدستی از آن استفاده کنند و اگر در این بین نیاز به تغییری بود، فقط یک ماژول نیاز به تغییر، داشته باشد.
برای این منظور، ابتدا متد login قبلی را طوری تغییر میدهیم که کار ذخیره سازی توکن را نیز در authService.js انجام دهد:
const tokenKey = "token"; export async function login(email, password) { const { data: { access_token } } = await http.post(apiEndpoint + "/login", { email, password }); console.log("JWT", access_token); localStorage.setItem(tokenKey, access_token); }
const { data } = this.state; await auth.login(data.username, data.password); window.location = "/";
همینکار را برای logout نیز در authService انجام داده:
export function logout() { localStorage.removeItem(tokenKey); }
import * as auth from "../services/authService"; class Logout extends Component { componentDidMount() { auth.logout();
import jwtDecode from "jwt-decode"; //... export function getCurrentUser() { try { const jwt = localStorage.getItem(tokenKey); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); return currentUser; } catch (ex) { console.log(ex); return null; } }
import * as auth from "./services/authService"; class App extends Component { state = {}; componentDidMount() { const currentUser = auth.getCurrentUser(); this.setState({ currentUser }); }
جای دیگری که از localStorage استفاده شده، متد doSumbit کامپوننت ثبت نام کاربران است. این قسمت را نیز به صورت زیر به authService اضافه میکنیم:
export function loginWithJwt(jwt) { localStorage.setItem(tokenKey, jwt); }
import * as auth from "../services/authService"; // ... const response = await userService.register(this.state.data); auth.loginWithJwt(response.headers["x-auth-token"]);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-27-backend.zip و sample-27-frontend.zip
public SettingsController(ISettings settings) { // example of saving _settings.General.SiteName = "دات نت تیپس"; _settings.Seo.HomeMetaTitle = ".Net Tips"; _settings.Seo.HomeMetaKeywords = "َAsp.net MVC,Entity Framework,Reflection"; _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه"; _settings.Save(); }
- تنظیمات به صورت گروه بندی شده در کنار هم قرار گرفتهاند و یافتن تنظیمات برای زمانی که نیاز به دسترسی به آنها داریم، راحتتر و سادهتر خواهد بود.
- به این شکل تنظیمات قابل دسترس در یک گروه، از دیتابیس بازیابی خواهند شد.
اصلا چرا باید این تنظیمات را در دیتابیس ذخیره کنیم؟
- ساخت یک Asp.net Web Application
- ساخت مدل Setting و افزودن آن به کانتکست Entity Framework
- ساخت کلاس SettingBase برای بازیابی و ذخیره سازی تنظیمات با رفلکشن
- ساخت کلاس GenralSettins و SeoSettings که از کلاس SettingBase ارث بری کردهاند.
- ساخت کلاس Settings به منظور مدیریت تمام انواع تنظیمات
namespace DynamicSettingAPI.Models { public interface IUnitOfWork { DbSet<Setting> Settings { get; set; } int SaveChanges(); } } public class ApplicationDbContext : IdentityDbContext<ApplicationUser>,IUnitOfWork { public DbSet<Setting> Settings { get; set; } public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } } namespace DynamicSettingAPI.Models { public class Setting { public string Name { get; set; } public string Type { get; set; } public string Value { get; set; } } }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Setting>() .HasKey(x => new { x.Name, x.Type }); modelBuilder.Entity<Setting>() .Property(x => x.Value) .IsOptional(); base.OnModelCreating(modelBuilder); }
namespace DynamicSettingAPI.Service { public abstract class SettingsBase { //1 private readonly string _name; private readonly PropertyInfo[] _properties; protected SettingsBase() { //2 var type = GetType(); _name = type.Name; _properties = type.GetProperties(); } public virtual void Load(IUnitOfWork unitOfWork) { //3 get setting for this type name var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList(); foreach (var propertyInfo in _properties) { //get the setting from setting list var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name); if (setting != null) { //4 set propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType)); } } } public virtual void Save(IUnitOfWork unitOfWork) { //5 get all setting for this type name var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList(); foreach (var propertyInfo in _properties) { var propertyValue = propertyInfo.GetValue(this, null); var value = (propertyValue == null) ? null : propertyValue.ToString(); var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name); if (setting != null) { // 6 update existing value setting.Value = value; } else { // 7 create new setting var newSetting = new Setting() { Name = propertyInfo.Name, Type = _name, Value = value, }; unitOfWork.Settings.Add(newSetting); } } } } }
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
public class GeneralSettings : SettingsBase { public string SiteName { get; set; } public string AdminEmail { get; set; } public bool RegisterUsersEnabled { get; set; } } public class GeneralSettings : SettingsBase { public string SiteName { get; set; } public string AdminEmail { get; set; } }
public interface ISettings { GeneralSettings General { get; } SeoSettings Seo { get; } void Save(); } public class Settings : ISettings { // 1 private readonly Lazy<GeneralSettings> _generalSettings; // 2 public GeneralSettings General { get { return _generalSettings.Value; } } private readonly Lazy<SeoSettings> _seoSettings; public SeoSettings Seo { get { return _seoSettings.Value; } } private readonly IUnitOfWork _unitOfWork; public Settings(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; // 3 _generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>); _seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>); } public void Save() { // only save changes to settings that have been loaded if (_generalSettings.IsValueCreated) _generalSettings.Value.Save(_unitOfWork); if (_seoSettings.IsValueCreated) _seoSettings.Value.Save(_unitOfWork); _unitOfWork.SaveChanges(); } // 4 private T CreateSettings<T>() where T : SettingsBase, new() { var settings = new T(); settings.Load(_unitOfWork); return settings; } }
private readonly ICache _cache; public Settings(IUnitOfWork unitOfWork, ICache cache) { // ARGUMENT CHECKING SKIPPED FOR BREVITY _unitOfWork = unitOfWork; _cache = cache; _generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>); _seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>); } private T CreateSettingsWithCache<T>() where T : SettingsBase, new() { // this is where you would implement loading from ICache throw new NotImplementedException(); }
public ActionResult Index() { using (var uow = new ApplicationDbContext()) { var _settings = new Settings(uow); _settings.General.SiteName = "دات نت تیپس"; _settings.General.AdminEmail = "admin@gmail.com"; _settings.General.RegisterUsersEnabled = true; _settings.Seo.HomeMetaTitle = ".Net Tips"; _settings.Seo.MetaKeywords = "Asp.net MVC,Entity Framework,Reflection"; _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه"; var settings2 = new Settings(uow); var output = string.Format("SiteName: {0} HomeMetaDescription: {1} MetaKeywords: {2} MetaTitle: {3} RegisterEnable: {4}", settings2.General.SiteName, settings2.Seo.HomeMetaDescription, settings2.Seo.MetaKeywords, settings2.Seo.HomeMetaTitle, settings2.General.RegisterUsersEnabled.ToString() ); return Content(output); } }