<button data-perm="true" data-controller="c" data-action="r" data-method="post" data-type="disable">Test 1</button> <button data-perm="true" data-controller="c" data-action="t" data-method="post">Test 2</button> <button data-perm="false" data-controller="c" data-action="r" data-method="post">Test 3</button> <a data-perm="true" data-controller="c" data-action="m" data-method="post">Test 4</a> <a data-perm="false" data-controller="c" data-action="m" data-method="post">Test 5</a> <a data-perm="true" data-controller="c" data-action="t" data-method="post">Test 6</a>
ویژگی | توضیحات |
perm | آیا نیاز به اعتبارسنجی دارد یا خیر؟ در صورتی که المانی مقدار Perm آن با مقدار true پر گردد، اعتبارسنجی روی آن اعمال خواهد شد. |
controller | نام کنترلری که به آن دسترسی دارد. |
action | نام اکشنی که به آن در کنترلر ذکر شده دسترسی دارد. |
method | در صورتیکه دسترسی get و post و ... هر یک متفاوت باشد. |
type | نحوه برخورد با المان غیرمجاز. در صورتیکه با disable مقداردهی شود، المان غیرفعال و در غیر اینصورت، از روی صفحه حذف میشود. |
سپس یک کلاس جدید ساخته و با ارث بری از ActionFilterAttribute، کار ساخت فیلتر را آغاز میکنیم:
public class AuthorizePage: ActionFilterAttribute { private HtmlTextWriter _htmlTextWriter; private StringWriter _stringWriter; private StringBuilder _stringBuilder; private HttpWriter _output; IAuthorization _auth; public override void OnActionExecuting(ActionExecutingContext filterContext) { _stringBuilder = new StringBuilder(); _stringWriter = new StringWriter(_stringBuilder); _htmlTextWriter = new HtmlTextWriter(_stringWriter); _output = (HttpWriter) filterContext.RequestContext.HttpContext.Response.Output; filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter; _auth = new Auth(); } public override void OnResultExecuted(ResultExecutedContext filterContext) { var response = _stringBuilder.ToString(); response = AuthorizeTags(response); _output.Write(response); } public string AuthorizeTags(string response) { var doc = GetHtmlDocument(response); var nodes=doc.DocumentNode.SelectNodes("//*[@data-perm]"); if (nodes == null) return response; foreach(var node in nodes) { var dataPermission = node.Attributes["data-perm"]; if(!dataPermission.Value.TryBooleanParse()) { continue; } var controller = node.Attributes["data-controller"].Value; var action = node.Attributes["data-action"].Value; var method = node.Attributes["data-method"].Value; var access=_auth.Authorize(HttpContext.Current.User.Identity.Name , controller, action, method); if (access) continue; var removeElm = true; var type = node.Attributes["data-type"]?.Value; if (type!=null && type.ToLower()== "disable") { removeElm = false; } if(removeElm) { node.Remove(); continue; } node.Attributes.Add("disabled", "true"); } return doc.DocumentNode.OuterHtml; } private HtmlDocument GetHtmlDocument(string htmlContent) { var doc = new HtmlDocument { OptionOutputAsXml = true, OptionDefaultStreamEncoding = Encoding.UTF8 }; doc.LoadHtml(htmlContent); return doc; } }
public interface IAuthorization { bool Authorize(string userId, string controller, string action, string method); }
ادامه کد در متد OnResultExecuted قرار دارد و متد اصلی کار ما میباشد. این متد بعد از صدور خروجی از اکشن، صدا زده شده اجرا میشود و شامل خروجی اکشن میباشد. خروجی اکشن را به متدی به نام AuthorizeResponse داده و با استفاده از بسته htmlagilitypack که یک HTML Parser میباشد، کدهای HTML را تحلیل میکنیم. قاعده فیلترسازی المانها در این کتابخانه بر اساس قواعد تعریف شده در XPath میباشد. بر اساس این قاعده ما گفتیم هر نوع تگی که دارای ویژگی data-perm میباشد، باید به عنوان گرههای فیلتر شده برگشت داده شود. سپس مقادیر نام کنترلر و اکشن و ... از المان دریافت شده و با استفاده از اینترفیسی که ما اینجا تعریف کردهایم، بررسی میکنیم که آیا این کاربر به این موارد دسترسی دارد یا خیر. در صورتیکه پاسخ برگشتی، از عدم اعتبار کاربر بگوید، گره مورد نظر حذف و یا در صورتیکه ویژگی data-type وجود داشته و مقدارش برابر disable باشد، آن المان غیرفعال خواهد شد. در نهایت کد تولیدی سند را به رشته تبدیل کرده و جایگزین خروجی فعلی میکنیم.
protected void Application_Start() { GlobalFilters.Filters.Add(new AuthorizePage()); }
استفاده از DDL Trigger
امکان ایجاد Trigger برای عملیات (DDL(Data Definition Language از SQL Server 2005 فراهم گردید. عملیاتی مانند ایجاد یک جدول جدید در بانک اطلاعاتی، اضافه شدن یک Login جدید و یا ایجاد یک بانک اطلاعاتی جدید را به وسیله این نوع Triggerها میتوان کنترل نمود. در حقیقت DDL Trigger به شما اجازه میدهد که از تاثیر تعدادی از دستورات DDL جلوگیری کنید. بدین ترتیب که تقریباً هر دستور DDL به طور خودکار، تراکنشی (Transactional) اجرا میشود . میتوان با دستور ROLLBACK TRANSACTION اجرای دستور DDL را لغو نمود. توجه شود همه دستورات DDL به صورت تراکنشی اجرا نمیشوند، به عنوان مثال دستور ALTER DATABASE ممکن است Database را تغییر دهد. در این صورت ساختار فایلی Database را تغییر میدهد، از آنجائی که سیستم عامل ویندوز به صورت تراکنشی عمل نمیکند بنابراین شما نمیتوانید این عمل فایل سیستمی را لغو نمائید. به هر حال شما میتوانید Trigger را با ALTER DATABASE فعال (fire) کنید برای عملیات Auditing، ولی نمیتوان از انجام عمل ALTER DATABASE جلوگیری کرد.برای نمونه میخواهیم از حذف و یا تغییر جداول یک بانک اطلاعاتی که به صورت عملیاتی در حال سرویس دهی است جلوگیری کنیم، برای اینکار از دستورهای زیر استفاده میکنیم:
create trigger Prevent_AlterDrop on database for drop_table, alter_table as print 'table can not be dropped or altered' rollback transaction
1- معرفی تابع ()EVENTDATA
این تابع، یک تابع سیستمی مهم است که در DDL Trigger استفاده میشود. در حالیکه DDL Trigger در هر سطحی فعال (fire) شود تابع سیستمی ()EVENTDATA فراخوانی (raise) میشود. خروجی تابع در قالب XML است. میتوان اطلاعات را از تابع EVENTDATA دریافت کرد و آنها را در یک جدول با فیلدی از جنس XML و یا با استفاده از XPath Query ثبت کرد (Logging). عناصر کلیدی (Key Elements) تابع EVENTDATA به شرح زیر است:• EventType: نوع رویدادی که باعث فراخوانی Trigger شده است.
• PostTime: زمانی که رویداد رخ میدهد.
• SPID :SPID کاربری که باعث ایجاد رویداد شده است.
• ServerName: نام SQL Instance که رویداد در آن رخ داده است.
• LoginName: نام Login که عمل مربوط به وقوع رویداد را اجرا میکند.
• UserName: نام User که عمل مربوط به وقوع رویداد را اجرا میکند.
• DatabaseName: نام Database که رویداد در آن رخ میدهد.
• ObjectType: نوع Object که اصلاح، حذف و یا ایجاد شده است.
• ObjectName: نام Object که اصلاح، حذف و یا ایجاد شده است.
• TSQLCommand: دستور T-SQL که اجرا شده و باعث اجرا شدن Trigger شده است.
2- بررسی یک سناریو نمونه
برای نمونه در دستورات زیر جدولی با نام ddl_log
CREATE TABLE ddl_log ( EventType nvarchar(100), PostTime datetime, SPID nvarchar(100), ServerName nvarchar(100), LoginName nvarchar(100), UserName nvarchar(100), DatabaseName nvarchar(100), ObjectName nvarchar(100), ObjectType nvarchar(100), DefaultSchema nvarchar(100), [SID] nvarchar(100), TSQLCommand nvarchar(2000));
CREATE TRIGGER [Log] ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS DECLARE @data XML SET @data = EVENTDATA() INSERT INTO ddl_log VALUES ( @data.value('(/EVENT_INSTANCE/EventType)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/PostTime)[1]', 'datetime'), @data.value('(/EVENT_INSTANCE/SPID)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/ServerName)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/LoginName)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/UserName)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/ObjectType)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/DefaultSchema)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/SID)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'nvarchar(2000)'));
3- ملاحظات
در صورت فعال شدن Trigger میتوان برخی موارد مانند محدودیت زمانی، کاربر اجرا کننده و ... را اضافه نمود. برای مثال در دستور زیر اجازه تغییرات در این زمان ( بین 7:00 A.M. تا .8:00 P.M ) امکان پذیر نیست و در صورت اقدام پیغام خطا دریافت میکنید و دستورات Create لغو خواهند شد و اگر خارج از زمان فوق دستورات DDL را اجرا کنید دستورات به طور موفقیت آمیز اجرا میشود و البته تغییرات نیز Log میشوند.
پس از Overrdie کردن میتوانید مجدداً Trigger را فعال کنید:
4- معرفی DDL Event Groups:
برای مشاهده جزئیات بیشتر میتوانید به این لینک مراجعه کنید.
Server Level
DDL_SERVER_LEVEL_EVENTS
DDL_LINKED_SERVER_EVENTS
DDL_LINKED_SERVER_LOGIN_EVENTS
DDL_REMOTE_SERVER_EVENTS
DDL_EXTENDED_PROCEDURE_EVENTS
DDL_MESSAGE_EVENTS
DDL_ENDPOINT_EVENTS
DDL_SERVER_SECURITY_EVENTS
DDL_LOGIN_EVENTS
DDL_GDR_SERVER_EVENTS
DDL_AUTHORIZATION_SERVER_EVENT Database Level
Database Level
DDL_DATABASE_LEVEL_EVENTSDDL_TABLE_VIEW_EVENTS
DDL_TABLE-EVENTS
DDL_VIEW_EVENTS
DDL_INDEX_EVENTS
DDL_STATISTICS_EVENTS
DDL_DATABASE_SECURITY_EVENTS
DDL_CERTIFICATE_EVENTS
DDL_USER_EVENTS
DDL_ROLE_EVENTS
DDL_APPLICATION_ROLE_EVENTS
DDL_SCHEMA_EVENTS
DDL_GDR_DATABASE_EVENTS
DDL_AUTHORIZATION_DATABASE_EVENTS
DDL_FUNCTION_EVENTS
DDL_PROCEDUER_EVENTS
DDL_TRIGGER_EVENTS
DDL_PARTITION_EVENTS
DDL_PARTITION_FUNCTION_EVENTS
DDL_PARTITION_SCHEME_EVENTS
DDL_SSB_EVENTS
DDL_MESSAGE_TYPE_EVENTS
DDL_CONTRACT_EVENTS
DDL_QUEUE_EVENTS
DDL_SERVER_EVENTS
DDL_ROUTE_EVENTS
DDL_REMOTE_SERVICE_BINDING_EVENTS
DDL_XML_SCHEMA_COLEECTION_EVENTS
DDL_FULLTEXT_CATALOG_EVENTS
DDL_DEFAULT_EVENTS
DDL_EXTENDED_PROPERTY_EVENTS
DDL_PLAN_GUIDE_EVENTS
DDL_RULE_EVENTS
DDL_SYNONYM_EVENTS
DDL_EVENT_NOTIFICATION_EVENTS
DDL_ASSEMBLY_EVENTS
DDL_TYPE_EVENTS
کتابخانهی ViewEffects برای اندروید
string name = "User 1"; int age = 20; var personTuple = (name, age); Console.WriteLine(personTuple.Item1); // User 1 Console.WriteLine(personTuple.Item2); // 20
string name = "User 1"; int age = 20; var personTuple = (name: name, age:age); Console.WriteLine(personTuple.name); // User 1 Console.WriteLine(personTuple.age); // 20
این وضعیت در C# 7.1 بهبود یافتهاست و اکنون کامپایلر در صورت عدم ذکر صریح نام اعضای Tuple، قادر است این نامها را دقیقا بر اساس نام متغیرها، همانند قابلیتی که در مورد anonymous types وجود دارد، تعیین کند و حدس بزند:
string name = "User 1"; int age = 20; var personTuple = (name, age); Console.WriteLine(personTuple.name); // User 1 Console.WriteLine(personTuple.age); // 20
مثالهایی از حدس زدن نامهای اعضای Tuple در C# 7.1
مثال اول همان حدس زدن نامهای اعضای Tuple بر اساس نام متغیرهای محلی متناظر با آنها است.
مثال دوم بر اساس نام خواص یک شیء است که توسط یک نوع Tuple بازگشت داده میشود:
var p = new Person { Name = "User 1", Age = 20 }; var personTuple = (p.Name, p.Age); Console.WriteLine(personTuple.Name); Console.WriteLine(personTuple.Age);
در اینجا عملگر .? نیز پشتیبانی میشود:
Person p = null; var personTuple = (p?.Name, p?.Age); Console.WriteLine(personTuple.Name); // null Console.WriteLine(personTuple.Age); // null
مثال سوم همان مثال دوم است که در یک عبارت LINQ بکار رفتهاست:
var people = new List<Person> { new Person {Name = "User 1", Age = 42}, new Person {Name = "User 2", Age = 18}, new Person {Name = "User 3", Age = 21} }; var tuples = people .Select(person => ( person.Name, person.Age, NameAndAge: $"{person.Name} is {person.Age}" ) ); var name = tuples.First().Name; var age = tuples.First().Age; var nameAndAge = tuples.First().NameAndAge;
نکته 1: حدس زدن نامها در مورد مقادیر خروجی متدها رخ نمیدهد.
در مثال ذیل نمیتوان به personTuple.FirstName بر اساس نام متد ذکر شده دسترسی یافت و تنها میتوان از Item1 در مورد آن استفاده کرد؛ اما در مورد متغیر محلی age میتوان:
int age = 42; var personTuple = (FirstName(), age); Console.WriteLine(personTuple.Item1); Console.WriteLine(personTuple.age);
نکته 2: اگر نام اعضای Tuple یکی باشند، عملیات حدس زدن نامها رخ نمیدهد.
var p1 = new Person { Name = "User 1", Age = 20 }; var p2 = new Person { Name = "User 2", Age = 22 }; var personTuple = (p1.Name, p2.Name); Console.WriteLine(personTuple.Item1); // User 1 Console.WriteLine(personTuple.Item2); // User 2
فرض کنید، چنین تنظیماتی را تدارک دیدهاید:
"CustomConfig": { "Setting1": "Hello", "Setting2": "Hello" }
public class CustomConfig { [Required(ErrorMessage = "Custom Error Message")] public string Setting1 { get; set; } public string Setting2 { get; set; } public string Setting3 { get; set; } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddOptions<CustomConfig>() .Bind(Configuration.GetSection("CustomConfig")) .ValidateDataAnnotations(); }
همچنین اگر نیاز به تعریف اعتبارسنجی سفارشی نیز وجود داشت میتوان به صورت زیر عمل کرد:
namespace MvcHealthCheckTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddOptions<CustomConfig>() .Bind(Configuration.GetSection("CustomConfig")) .ValidateDataAnnotations() .Validate(customConfig => { if (customConfig.Setting2 != default) { return customConfig.Setting3 != default; } return true; }, "Setting 3 is required when Setting2 is present"); }
ASP.NET MVC #19
<caching> <outputCacheSettings> <outputCacheProfiles> <add name="Dashboard" duration="86400" varyByParam="*" varyByCustom="User" location="Server" /> </outputCacheProfiles> </outputCacheSettings> </caching>
[OutputCache(CacheProfile="Dashboard")] public class DashboardController : Controller { ...}
//string arg filled with the value of "varyByCustom" in your web.config public override string GetVaryByCustomString(HttpContext context, string arg) { if (arg == "User") { // depends on your authentication mechanism return "User=" + context.User.Identity.Name; //?return "User=" + context.Session.SessionID; } return base.GetVaryByCustomString(context, arg); }
string username = ConfigurationManager.AppSettings["GoogleAnalyticsUsername"]; string password = ConfigurationManager.AppSettings["GoogleAnalyticsPassword"]; string profileId = ConfigurationManager.AppSettings["GoogleAnalyticsProfileId"]; string yesterdayDate = DateTime.Today.AddDays(-1).ToString("yyyy-MM-dd"); var service = new AnalyticsService("WebSiteAnalytics"); service.setUserCredentials(username, password); var dataQuery = new DataQuery() { Ids = profileId, Metrics = "ga:pageviews", Sort = "ga:pageviews", }; DataFeed dataFeed = null; //--- total visit dataQuery.GAStartDate = new DateTime(2012, 3, 1).ToString("yyyy-MM-dd"); dataQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd"); dataFeed = service.Query(dataQuery); var totalEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" : (dataFeed.Entries[0] as DataEntry).Metrics[0].Value; //--- yesterday visit dataQuery.GAStartDate = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd"); dataQuery.GAEndDate = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd"); dataFeed = service.Query(dataQuery); var yesterdayEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" : (dataFeed.Entries[0] as DataEntry).Metrics[0].Value; //--- today visit dataQuery.GAStartDate = DateTime.Now.ToString("yyyy-MM-dd"); dataQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd"); dataFeed = service.Query(dataQuery); var todayEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" : (dataFeed.Entries[0] as DataEntry).Metrics[0].Value; //--- last week visit dataQuery.GAStartDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd"); dataQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd"); dataFeed = service.Query(dataQuery); var lastWeekEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" : (dataFeed.Entries[0] as DataEntry).Metrics[0].Value; //--- last month visit dataQuery.GAStartDate = DateTime.Now.AddDays(-30).ToString("yyyy-MM-dd"); dataQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd"); dataFeed = service.Query(dataQuery); var lastMonthEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" : (dataFeed.Entries[0] as DataEntry).Metrics[0].Value;
پیاده سازی SoftDelete در EF Core
حل مشکل حذف منطقی در جداولی که فیلد منحصر به فرد دارند
[Index(nameof(PhoneNumber), IsUnique = true)] public class User { public long Id { get; set; } [Required] [MaxLength(100)] public string FullName { get; set; } = null!; [Required] [MaxLength(11)] public string PhoneNumber { get; set; } = null!; public bool IsDeleted { get; set; } }
در موجودیت User فیلد PhoneNumber منحصر به فرد است.
کاربری با شماره تلفن 123 ثبت نام میکند و بعد حذف میشود
الان شماره تلفن 123 آزاد است و کاربر دیگری میتواند با این شماره ثبت نام کند و این کاربر نیز حذف میشود و یک کاربر جدید با شماره تلفن 123 ثبت نام میکند
FullName PhoneNumber IsDeleted User1 123 True User2 123 True User3 123 False
پس با وجود اینکه شماره تلفن Unique است ولی میتوان شماره تلفن تکراری داشت به شرطی که IsDeleted=True باشد و باید فقط یک شماره تلفن 123 با IsDeleted=False داشت.
برای اینکار از Filter ها استفاده میکنیم و تنها کافیست در فایل Configuration موجودیت User این کد را اضافه کرد
public class UserConfiguration : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { builder.HasIndex(x => x.PhoneNumber) .HasFilter("[IsDeleted] = 0"); } }
الان شرط Unique بودن فقط در صورتی کار میکند که IsDeleted=False باشد.
راه اندازی StimulSoft Report در ASP.NET MVC
public class CustomerServiceReport { public string ReportTitle { get; set; } public string ReportDate { get; set; } //... public List<CustomerRow> Customers { get; set; } = new List<CustomerRow>(); } public class CustomerRow { public int CustomerId { get; set; } public string CustomerName { get; set; } public string Phone { get; set; } //... public List<ServiceRow> Services { get; set; } = new List<ServiceRow>();
} public class ServiceRow { public int ServiceId { get; set; } public string ServiceName { get; set; } public int Count { get; set; } public long Price { get; set; } }
var data = new CustomerServiceReport { ReportDate = "1399/09/04", ReportTitle = "گزارش سرویسهای مشتریان" }; for (int i = 0; i < 5; i++) { var customer = new CustomerRow { CustomerId = i + 1, CustomerName = $"مشتری شماره {i + 1}", Phone = "001122" }; for (int j = 0; j < 3; j++) { var service = new ServiceRow { ServiceId = j + 1, ServiceName = $"سرویس شماره {j + 1}", Count = (j + 1) * 10, Price = (j + 1) * 10000 }; customer.Services.Add(service); //اضافه کردن سرویس به هر مشتری } data.Customers.Add(customer); // اضافه کردن مشتری به گزارش } var report = new StiReport(); report.RegBusinessObject("CustomerServiceReport", data); report.Dictionary.SynchronizeBusinessObjects();