dotnet new -i FeatherHttp.Templates::0.1.67-alpha.g69b43bed72 --nuget-source https://f.feedz.io/featherhttp/framework/nuget/index.json
Templates Short Name Language Tags ---------------------------------------------------------------------------------------------------------------------------------- FeatherHttp feather [C#] Web/ASP.NET/FeatherHttp
dotnet new feather --name todoAPI
همانطور که مشاهده میکنید پروژهی فوق تنها شامل دو فایل .csproj و Program.cs است. درون Program.cs و متد Main کار initialize کردن سرور HTTP صورت گرفته است. WebApplication.Create دقیقا همانند Host.CreateDefaultBuilder پروژههای ASP.NET Core عمل میکند؛ یعنی پیکربندی pipeline از قبیل اضافه کردن متغیرهای محیطی، خواندن از فایل JSON و ... را انجام میدهد اما با کد boilerplate کمتر. بنابراین خروجی WebApplication.Create یک ASP.NET Core Pipeline با قابلیت اضافه کردن تنظیمات دلخواه است. در ادامه جهت بررسی بیشتر Feather HTTP، یک مدل را به همراه یک سری دیتای In-memory به پروژه اضافه خواهیم کرد:
using System.Collections.Generic; using System.Text.Json.Serialization; using System.Linq; namespace todoAPI.Models { public class Todo { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("title")] public string Title { get; set; } [JsonPropertyName("completed")] public bool Completed { get; set; } } public class TodoData { private readonly IList<Todo> _db = new List<Todo> { new Todo { Id = 1, Title = "Read book" }, new Todo { Id = 2, Title = "Watch an episode of Dark" }, new Todo { Id = 3, Title = "Publish a post on dotnettips" }, new Todo { Id = 4, Title = "Skype with my friend" }, }; public IList<Todo> GetAllToDoItmes() { return _db; } public void AddTodo(Todo item) { _db.Add(item); } public void ToggleTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); todo.Completed = !todo.Completed; } public void DeleteTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); _db.Remove(todo); } } }
در مثال فوق برای نگاشت نام خواص، از System.Text.Json توکار NET Core 3.0. استفاده شدهاست. در ادامه نیز از یک کلاس برای شبیهسازی CRUD یک Todo استفاده شدهاست. سپس برای داشتن اندپوینتهای موردنظر به ازای هر کدام از متدهای فوق درون متد Main، از app.Map... استفاده کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using todoAPI.Models; namespace todoAPI { class Program { private static readonly TodoData db = new TodoData(); static async Task Main(string[] args) { var app = WebApplication.Create(args); app.MapGet("/", GetTodos); app.MapPost("/api/todos", CreateTodo); app.MapPost("/api/todos/{id}", ToggleTodo); app.MapDelete("/api/todos/{id}", DeleteTodo); await app.RunAsync(); } static async Task GetTodos(HttpContext http) { var todos = db.GetAllToDoItmes(); await http.Response.WriteJsonAsync(todos); } static async Task CreateTodo(HttpContext http) { var todo = await http.Request.ReadJsonAsync<Todo>(); db.AddTodo(todo); http.Response.StatusCode = 204; } static async Task ToggleTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } db.ToggleTodo(id); http.Response.StatusCode = 204; } static async Task DeleteTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } db.DeleteTodo(id); http.Response.StatusCode = 204; } } }
هر کدام از اندپوینتهای فوق، یک ورودی HttpContext دریافت خواهند کرد. توسط این شیء میتوانیم به درخواست جاری و همچنین به پاسخ درخواست، دسترسی داشته باشیم.
استفاده از سیستم DI توکار NET Core.
همانطور که در ابتدای مطلب نیز عنوان شد، Feather HTTP یک wrapper بر روی APIهای موجود ASP.NET Core است، بنابراین میتوانیم از همان سرویس DI که درون پروژههای ASP.NET Core در اختیار داریم در اینجا نیز استفاده کنیم. در ادامه یک پوشهی جدید را به مثال قبل، با نام Controllers اضافه خواهیم کرد و درون آن یک فایل TodoController را با محتویات زیر ایجاد خواهیم کرد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using todoAPI.Models; using todoAPI.Services; namespace todoAPI.Controllers { public class TodoController { private readonly ITodoService _todoService; public TodoController(ITodoService todoService) { _todoService = todoService; } public async Task GetTodos(HttpContext http) { var todos = _todoService.GetAllToDoItmes(); await http.Response.WriteJsonAsync(todos); } public async Task CreateTodo(HttpContext http) { var todo = await http.Request.ReadJsonAsync<Todo>(); _todoService.AddTodo(todo); http.Response.StatusCode = 204; } public async Task ToggleTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } _todoService.ToggleTodo(id); http.Response.StatusCode = 204; } public async Task DeleteTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } _todoService.DeleteTodo(id); http.Response.StatusCode = 204; } } }
کاری که انجام شده است، انتقال تمامی متدهای static به کلاس فوق و سپس جایگزین کردن کلمهی کلیدی static با public است. همچنین یه ارجاع به اینترفیس جدید با عنوان ITodoService اضافه شده است؛ درون پیادهسازی این اینترفیس همان متدهای کلاس TodoData را اضافه کردهایم:
using System.Collections.Generic; using todoAPI.Models; using System.Linq; namespace todoAPI.Services { public interface ITodoService { void AddTodo(Todo item); void DeleteTodo(int id); IList<Todo> GetAllToDoItmes(); void ToggleTodo(int id); } public class TodoService : ITodoService { private readonly IList<Todo> _db = new List<Todo> { new Todo { Id = 1, Title = "Read book" }, new Todo { Id = 2, Title = "Watch an episode of Dark" }, new Todo { Id = 3, Title = "Publish a post on dotnettips" }, new Todo { Id = 4, Title = "Skype with my friend" }, }; public IList<Todo> GetAllToDoItmes() { return _db; } public void AddTodo(Todo item) { _db.Add(item); } public void ToggleTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); todo.Completed = !todo.Completed; } public void DeleteTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); _db.Remove(todo); } } }
نکته: برای ایجاد اینترفیس از روی یک کلاس درون VS Code میتوانیم اینگونه عمل کنیم:
تغییرات فایل Program.cs
ابتدا باید using مربوط به DI را در ابتدای فایل اضافه کنیم:
using Microsoft.Extensions.DependencyInjection;
سپس توسط ServiceProvider یک وهله از کلاس موردنظر را ایجاد کردهایم و همچنین سرویسهای موردنظر را درون DI Container اضافه کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using todoAPI.Controllers; using todoAPI.Services; namespace todoAPI { class Program { static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddTransient<TodoController>(); builder.Services.AddTransient<ITodoService, TodoService>(); var serviceProvider = builder.Services.BuildServiceProvider(); var todoController = serviceProvider.GetService<TodoController>(); var app = WebApplication.Create(args); app.MapGet("/", todoController.GetTodos); app.MapPost("/api/todos", todoController.CreateTodo); app.MapPost("/api/todos/{id}", todoController.ToggleTodo); app.MapDelete("/api/todos/{id}", todoController.DeleteTodo); await app.RunAsync(); } } }
Convention Over Configuration
در کد قبلی به صورت دستی TodoController را توسط Service Location از DI درخواست کردهایم. اینکار را در ادامه میتوانیم به Feather HTTP سپرده تا کار وهلهسازی را براساس قواعد توکار برایمان انجام دهد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using todoAPI.Services; namespace todoAPI { class Program { static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); builder.Services.AddControllers(); builder.Services.AddSingleton<ITodoService, TodoService>(); var serviceProvider = builder.Services.BuildServiceProvider(); var app = builder.Build(); app.MapControllers(); await app.RunAsync(); } } }
سپس در ادامه برای دسترسی به HTTP Context درون TodoController از IHttpContextAccessor استفاده کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using todoAPI.Models; using todoAPI.Services; namespace todoAPI.Controllers { public class TodoController { private readonly ITodoService _todoService; private readonly IHttpContextAccessor _accessor; public TodoController(ITodoService todoService, IHttpContextAccessor accessor) { _todoService = todoService; _accessor = accessor; } [HttpGet("/todos")] public async Task GetTodos() { var todos = _todoService.GetAllToDoItmes(); await _accessor.HttpContext.Response.WriteJsonAsync(todos); } [HttpPost("/todos")] public async Task CreateTodo() { var todo = await _accessor.HttpContext.Request.ReadJsonAsync<Todo>(); _todoService.AddTodo(todo); _accessor.HttpContext.Response.StatusCode = 204; } [HttpPost("/todos/{id}")] public async Task ToggleTodo(int id) { _todoService.ToggleTodo(id); _accessor.HttpContext.Response.StatusCode = 204; } [HttpDelete("/todos/{id}")] public async Task DeleteTodo(int id) { _todoService.DeleteTodo(id); _accessor.HttpContext.Response.StatusCode = 204; } } }
کدهای کامل مطلب را میتوانید از اینجا دریافت کنید.
<meta name="fragment" content="!"> <base href="/">
http://www.example.com/?_escaped_fragment_=
http://www.example.com/user/123?_escaped_fragment_=
DynamicModuleUtility.RegisterModule(typeof(Prerender.io.PrerenderModule));
http://www.example.com/user/123?_escaped_fragment_=
<package id="PhantomJS" version="1.9.2" targetFramework="net452" /> <package id="phantomjs.exe" version="1.9.2.1" targetFramework="net452" />
public class AjaxCrawlableAttribute : System.Web.Mvc.ActionFilterAttribute { private const string Fragment = "_escaped_fragment_"; public override void OnActionExecuting(ActionExecutingContext filterContext) { var request = filterContext.RequestContext.HttpContext.Request; var url = request.Url.ToString(); if (request.QueryString[Fragment] != null && !url.Contains("HtmlSnapshot/returnHTML")) { url = url.Replace("?_escaped_fragment_=", string.Empty).Replace(request.Url.Scheme + "://", string.Empty); url = url.Split(':')[1]; filterContext.Result = new RedirectToRouteResult( new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } }); } return; } }
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "HtmlSnapshot", url: "HtmlSnapshot/returnHTML/{*url}", defaults: new { controller = "HtmlSnapshot", action = "returnHTML", url = UrlParameter.Optional }); routes.MapRoute( name: "SPA", url: "{*catchall}", defaults: new { controller = "Home", action = "Index" }) }
public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new AjaxCrawlableAttribute()); } }
public ActionResult returnHTML(string url) { var prefix = HttpContext.Request.Url.Scheme + "://" + HttpContext.Request.Url.Host+":"; url = prefix+url; string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); var startInfo = new ProcessStartInfo { Arguments = string.Format("{0} {1}", Path.Combine(appRoot, "Scripts\\seo.js"), url), FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"), UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, StandardOutputEncoding = System.Text.Encoding.UTF8 }; var p = new Process(); p.StartInfo = startInfo; p.Start(); string output1 = p.StandardOutput.ReadToEnd(); p.WaitForExit(); ViewData["result"] = output1.Replace("<!-- ngView: -->", "").Replace("ng-view=\"\"", ""); return View(); }
@{ Layout = null; } @Html.Raw(ViewBag.result)
console.log(page.content)
var page = require('webpage').create(); var system = require('system'); var lastReceived = new Date().getTime(); var requestCount = 0; var responseCount = 0; var requestIds = []; var startTime = new Date().getTime();; page.onResourceReceived = function (response) { if (requestIds.indexOf(response.id) !== -1) { lastReceived = new Date().getTime(); responseCount++; requestIds[requestIds.indexOf(response.id)] = null; } }; page.onResourceRequested = function (request) { if (requestIds.indexOf(request.id) === -1) { requestIds.push(request.id); requestCount++; } }; function checkLoaded() { return page.evaluate(function () { return document.all["compositionComplete"]; }) != null; } // Open the page page.open(system.args[1], function () { }); var checkComplete = function () { // We don't allow it to take longer than 5 seconds but // don't return until all requests are finished if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) { clearInterval(checkCompleteInterval); console.log(page.content); phantom.exit(); } } // Let us check to see if the page is finished rendering var checkCompleteInterval = setInterval(checkComplete, 300);
<!DOCTYPE html> <html ng-app="appOne"> <head> <meta name="fragment" content="!"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta charset="utf-8" /> <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" /> <meta name="viewport" content="width=device-width" /> <base href="/"> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") <script src="~/Scripts/angular/angular.js"></script> <script src="~/Scripts/angular/angular-route.js"></script> <script src="~/Scripts/angular/angular-animate.js"></script> <script> angular.module('appOne', ['ngRoute'], function ($routeProvider, $locationProvider) { $routeProvider.when('/one', { template: "<div>one</div>", controller: function ($scope) { } }) .when('/two', { template: "<div>two</div>", controller: function ($scope) { } }).when('/', { template: "<div>home</div>", controller: function ($scope) { } }); $locationProvider.html5Mode({ enabled: true }); }); </script> </head> <body> <div id="body"> <section ng-view></section> @RenderBody() </div> <div id="footer"> <ul class='xoxo blogroll'> <li><a href="one">one</a></li> <li><a href="two">two</a></li> </ul> </div> </body> </html>
- جایگزین آن ViewComponet است و از لحاظ دسترسی به منابع و سرویسها محدودیتی ندارد. یک مثال
- هنوز هم اگر صرفا نیاز به رندر یک پارشال View را به صورت Ajax ایی دارید، روش زیر کار میکند:
جایی که میخواستید Html.RenderAction را قرار دهید، قطعه کد Ajax ایی زیر را فراخوانی کنید:
<div id="dynamicContentContainer"></div> <script> $.get('@Url.Action("GetData", "Home")', {id : 1}, function(content){ $("#dynamicContentContainer").html(content); }); </script>
[HttpGet] public IActionResult GetData(int id) { return PartialView(id); }
@model int <span>Values from controler :</span> @Model
الف) دریافت کدهای EFTracingProvider اصلی: (+)
از کدهای دریافتی این مجموعه، فقط به دو پوشه EFTracingProvider و EFProviderWrapperToolkit آن نیاز است.
ب) اصلاح کوچکی در کدهای این پروایدر جهت بررسی نال بودن شیءایی که باید dispose شود
در فایل DbConnectionWrapper.cs، متد Dispose را یافته و به نحو زیر اصلاح کنید (بررسی نال نبودن wrappedConnection اضافه شده است):
protected override void Dispose(bool disposing) { if (disposing) { if (this.wrappedConnection != null) this.wrappedConnection.Dispose(); } base.Dispose(disposing); }
ج) ساخت یک کلاس پایه Context با قابلیت لاگ فرامین SQL صادره، جهت میسر سازی استفاده مجدد از کدهای آن
د) رفع خطای The given key was not present in the dictionary در حین استفاده از EFTracingProvider
در ادامه کدهای کامل این دو قسمت به همراه یک مثال کاربردی را ملاحظه میکنید:
using System; using System.Configuration; using System.Data; using System.Data.Common; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Entity.Migrations; using System.Diagnostics; using System.Linq; using EFTracingProvider; namespace Sample { public class Person { public int Id { get; set; } public string Name { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { var className = this.ContextType.FullName; var connectionStringData = ConfigurationManager.ConnectionStrings[className]; if (connectionStringData == null) throw new InvalidOperationException(string.Format("ConnectionStrings[{0}] not found.", className)); TargetDatabase = new DbConnectionInfo(connectionStringData.ConnectionString, connectionStringData.ProviderName); AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { for (int i = 0; i < 7; i++) context.Users.Add(new Person { Name = "name " + i }); base.Seed(context); } } public class MyContext : MyLoggedContext { public DbSet<Person> Users { get; set; } } public abstract class MyLoggedContext : DbContext { protected MyLoggedContext() : base(existingConnection: createConnection(), contextOwnsConnection: true) { var ctx = ((IObjectContextAdapter)this).ObjectContext; ctx.GetTracingConnection().CommandExecuting += (s, e) => { Console.WriteLine("{0}\n", e.ToTraceString()); }; } private static DbConnection createConnection() { var st = new StackTrace(); var sf = st.GetFrame(2); // Get the derived class Type in a base class static method var className = sf.GetMethod().DeclaringType.FullName; var connectionStringData = ConfigurationManager.ConnectionStrings[className]; if (connectionStringData == null) throw new InvalidOperationException(string.Format("ConnectionStrings[{0}] not found.", className)); if (!isEFTracingProviderRegistered()) EFTracingProviderConfiguration.RegisterProvider(); EFTracingProviderConfiguration.LogToFile = "log.sql"; var wrapperConnectionString = string.Format(@"wrappedProvider={0};{1}", connectionStringData.ProviderName, connectionStringData.ConnectionString); return new EFTracingConnection { ConnectionString = wrapperConnectionString }; } private static bool isEFTracingProviderRegistered() { var data = (DataSet)ConfigurationManager.GetSection("system.data"); var providerFactories = data.Tables["DbProviderFactories"]; return providerFactories.Rows.Cast<DataRow>() .Select(row => (string)row.ItemArray[1]) .Any(invariantName => invariantName == "EF Tracing Data Provider"); } } public static class Test { public static void RunTests() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var ctx = new MyContext()) { var users = ctx.Users.AsEnumerable(); if (users.Any()) { foreach (var user in users) { Console.WriteLine(user.Name); } } var rnd = new Random(); var user1 = ctx.Users.Find(1); user1.Name = "test user " + rnd.Next(); ctx.SaveChanges(); } } } }
توضیحات:
تعریف TargetDatabase در Configuration سبب میشود تا خطای The given key was not present in the dictionary در حین استفاده از این پروایدر جدید برطرف شود. به علاوه همانطور که ملاحظه میکنید اطلاعات رشته اتصالی بر اساس قراردادهای توکار EF Code first به نام کلاس Context تنظیم شده است.
کلاس MyLoggedContext، کلاس پایهای است که تنظیمات اصلی «EF Tracing Data Provider» در آن قرار گرفتهاند. برای استفاده از آن باید رشته اتصالی مخصوصی تولید و در اختیار کلاس پایه DbContext قرار گیرد (توسط متد createConnection ذکر شده).
به علاوه در اینجا توسط خاصیت EFTracingProviderConfiguration.LogToFile میتوان نام فایلی را که قرار است عبارات SQL تولیدی در آن درج شوند، ذکر نمود. همچنین یک روش دیگر دستیابی به کلیه عبارات SQL تولیدی را با مقدار دهی CommandExecuting در سازنده کلاس مشاهده میکنید.
اکنون که این کلاس پایه تهیه شده است، تنها کافی است Context معمولی برنامه به نحو زیر تعریف شود:
public class MyContext : MyLoggedContext
insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 0" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 1" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 2" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 3" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 4" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 5" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 6" SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[People] AS [Extent1] SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[People] AS [Extent1] name 0 name 1 name 2 name 3 name 4 name 5 name 6 update [dbo].[People] set [Name] = @0 where ([Id] = @1) -- @0 (dbtype=String, size=-1, direction=Input) = "test user 1355460609" -- @1 (dbtype=Int32, size=0, direction=Input) = 1
قسمتی از این خروجی مرتبط است به متد Seed تعریف شده که تعدادی رکورد را در بانک اطلاعاتی ثبت میکند.
دو select نیز در انتهای کار قابل مشاهده است. اولین مورد به علت فراخوانی متد Any صادر شده است و دیگری به حلقه foreach مرتبط میباشد (چون از AsEnumerable استفاده شده، هربار ارجاع به شیء users، یک رفت و برگشت به بانک اطلاعاتی را سبب خواهد شد. برای رفع این حالت میتوان از متد ToList استفاده کرد.)
در پایان کار، متد update مربوط است به فراخوانی متدهای find و save changes ذکر شده. این خروجی در فایل sql.log نیز در کنار فایل اجرایی برنامه ثبت شده و قابل مشاهده میباشد.
کاربردها
اطلاعات این مثال میتواند پایه نوشتن یک برنامه entity framework profiler باشد.
[HttpGet] public string SelectAllJqTree() { var result = _kpiTypeService.SelectAll().Select(e => new { value = e.Id, text = e.Name, }); var select = "<select>{0}</select>"; var option = "<option value='{0}' >{1}</option>"; var options = ""; foreach (var item in result) { options += string.Format(option, item.value, item.text); } return string.Format(select, options).Trim(); }
colModel: [ { name: 'KpiTypeID', index: 'KpiTypeID', align: 'right', width: 300, editable: true, edittype: 'select', editoptions: { dataUrl: '/api/KPIType/SelectAllJqTree' }, editrules: { required: true } } ]
$.ajax({ url: "/api/KPIType/SelectAllJqTree", context: document.body }).done(function (data) { $("#selection").html(data); });
اگر چندین درخواست با یک Session ID به موتور پردازشی ASP.NET وارد شوند و حداقل یکی از آنها به صورت سشنی خواندنی+نوشتنی علامت گذاری شده باشد، اینگونه درخواستها serialized میشود. به این معنا که این نوع درخواستها در صف قرار داده شده و به ترتیب رسیدن، اجرا میشوند. این کار هم از این جهت لازم است که چندین درخواست یا به عبارتی چندین ترد همزمان، سبب انجام تغییرات ناخواستهای در مقادیر سشن مورد استفاده نشوند.
و اگر چندین درخواست با یک Session ID به موتور پردازشی ASP.NET وارد شوند و تمام آنها سشن «فقط خواندنی» باشند یا در آنها سشن «غیرفعال» شده باشد، این درخواستها به صورت موازی پردازش خواهند شد.
بنابراین اگر در برنامه خود از سشن استفاده نمیکنید، سطر زیر را به وب کانفیگ برنامه اضافه نمائید تا حداکثر استفاده را از پردازش موازی خودکار کرده باشید:
<httpModules> <remove name="Session"/> </httpModules>
<sessionState mode="Off" />
و یا در ASP.NET MVC میتوان کنترل ریزتری را به نحو زیر نیز اعمال کرد:
[SessionState(SessionStateBehavior.Disabled)] public class AjaxTestController : Controller { //... }
نکته دیگر اینکه اگر نیاز به استفاده از سشن در یک اکشن متد وجود داشته باشد میتوان از فیلتر سشنهای فقط خواندنی استفاده کرد تا باز هم به همان پردازش موازی پشت صحنه برسیم:
[SessionState(SessionStateBehavior.ReadOnly)]
ثبت برنامهی خود در گوگل و انجام تنظیمات آن
اولین کاری که برای استفاده از نگارش سوم Google Analytics API باید صورت گیرد، ثبت برنامهی خود در Google Developer Console است. برای این منظور ابتدا به آدرس ذیل وارد شوید:
سپس بر روی دکمهی Create project کلیک کنید. نام دلخواهی را وارد کرده و در ادامه بر روی دکمهی Create کلیک نمائید تا پروفایل این پروژه ایجاد شود.
تنها نکتهی مهم این قسمت، بخاطر سپردن نام پروژه است. زیرا از آن جهت اتصال به API گوگل استفاده خواهد شد.
پس از ایجاد پروژه، به صفحهی آن وارد شوید و از منوی سمت چپ صفحه، گزینهی Credentials را انتخاب کنید. در ادامه در صفحهی باز شده، بر روی دکمهی Create new client id کلیک نمائید.
در صفحهی باز شده، گزینهی Service account را انتخاب کنید. اگر سایر گزینهها را انتخاب نمائید، کاربری که قرار است از API استفاده کند، باید بتواند توسط مرورگر نصب شدهی بر روی کامپیوتر اجرا کنندهی برنامه، یکبار به گوگل لاگین نماید که این مورد مطلوب برنامههای وب و همچنین سرویسها نیست.
در اینجا ابتدا یک فایل مجوز p12 را به صورت خودکار دریافت خواهید کرد و همچنین پس از ایجاد client id، نیاز است، ایمیل آنرا جایی یادداشت نمائید:
از این ایمیل و همچنین فایل p12 ارائه شده، جهت لاگین به سرور استفاده خواهد شد.
همچنین نیاز است تا به برگهی APIs پروژهی ایجاد شده رجوع کرد و گزینهی Analytics API آنرا فعال نمود:
تا اینجا کار ثبت و فعال سازی برنامهی خود در گوگل به پایان میرسد.
دادن دسترسی به Client ID ثبت شده در برنامهی Google Analytics
پس از اینکه Client ID سرویس خود را ثبت کردید، نیاز است به اکانت Google Analytics خود وارد شوید. سپس در منوی آن، گزینهی Admin را پیدا کرده و به آن قسمت، وارد شوید:
در ادامه به گزینهی User management آن وارد شده و به ایمیل Client ID ایجاد شده در قسمت قبل، دسترسی خواندن و آنالیز را اعطاء کنید:
در صورت عدم رعایت این مساله، کلاینت API، قادر به دسترسی به Google Analytics نخواهد بود.
استفاده از نگارش سوم Google Analytics API در دات نت
قسمت مهم کار، تنظیمات فوق است که در صورت عدم رعایت آنها، شاید نصف روزی را مشغول به دیباگ برنامه شوید. در ادامه نیاز است پیشنیازهای دسترسی به نگارش سوم Google Analytics API را نصب کنیم. برای این منظور، سه بستهی نیوگت ذیل را توسط کنسول پاورشل نیوگت، به برنامه اضافه کنید:
PM> Install-Package Google.Apis PM> Install-Package Google.Apis.auth PM> Install-Package Google.Apis.Analytics.v3
using System; using System.Linq; using System.Security.Cryptography.X509Certificates; using Google.Apis.Analytics.v3; using Google.Apis.Analytics.v3.Data; using Google.Apis.Auth.OAuth2; using Google.Apis.Services; namespace GoogleAnalyticsAPIv3Tests { public class AnalyticsQueryParameters { public DateTime Start { set; get; } public DateTime End { set; get; } public string Dimensions { set; get; } public string Filters { set; get; } public string Metrics { set; get; } } public class AnalyticsAuthentication { public Uri SiteUrl { set; get; } public string ApplicationName { set; get; } public string ServiceAccountEmail { set; get; } public string KeyFilePath { set; get; } public string KeyFilePassword { set; get; } public AnalyticsAuthentication() { KeyFilePassword = "notasecret"; } } public class GoogleAnalyticsApiV3 { public AnalyticsAuthentication Authentication { set; get; } public AnalyticsQueryParameters QueryParameters { set; get; } public GaData GetData() { var service = createAnalyticsService(); var profile = getProfile(service); var query = service.Data.Ga.Get("ga:" + profile.Id, QueryParameters.Start.ToString("yyyy-MM-dd"), QueryParameters.End.ToString("yyyy-MM-dd"), QueryParameters.Metrics); query.Dimensions = QueryParameters.Dimensions; query.Filters = QueryParameters.Filters; query.SamplingLevel = DataResource.GaResource.GetRequest.SamplingLevelEnum.HIGHERPRECISION; return query.Execute(); } private AnalyticsService createAnalyticsService() { var certificate = new X509Certificate2(Authentication.KeyFilePath, Authentication.KeyFilePassword, X509KeyStorageFlags.Exportable); var credential = new ServiceAccountCredential( new ServiceAccountCredential.Initializer(Authentication.ServiceAccountEmail) { Scopes = new[] { AnalyticsService.Scope.AnalyticsReadonly } }.FromCertificate(certificate)); return new AnalyticsService(new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = Authentication.ApplicationName }); } private Profile getProfile(AnalyticsService service) { var accountListRequest = service.Management.Accounts.List(); var accountList = accountListRequest.Execute(); var site = Authentication.SiteUrl.Host.ToLowerInvariant(); var account = accountList.Items.FirstOrDefault(x => x.Name.ToLowerInvariant().Contains(site)); var webPropertyListRequest = service.Management.Webproperties.List(account.Id); var webPropertyList = webPropertyListRequest.Execute(); var sitePropertyList = webPropertyList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site)); var profileListRequest = service.Management.Profiles.List(account.Id, sitePropertyList.Id); var profileList = profileListRequest.Execute(); return profileList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site)); } } }
مثالی از نحوه استفاده از کلاس GoogleAnalyticsApiV3
در ادامه یک برنامهی کنسول را ملاحظه میکنید که از کلاس GoogleAnalyticsApiV3 استفاده میکند:
using System; using System.Collections.Generic; using System.Linq; namespace GoogleAnalyticsAPIv3Tests { class Program { static void Main(string[] args) { var statistics = new GoogleAnalyticsApiV3 { Authentication = new AnalyticsAuthentication { ApplicationName = "My Project", KeyFilePath = "811e1d9976cd516b55-privatekey.p12", ServiceAccountEmail = "10152bng4j3mq@developer.gserviceaccount.com", SiteUrl = new Uri("https://www.dntips.ir/") }, QueryParameters = new AnalyticsQueryParameters { Start = DateTime.Now.AddDays(-7), End = DateTime.Now, Dimensions = "ga:date", Filters = null, Metrics = "ga:users,ga:sessions,ga:pageviews" } }.GetData(); foreach (var result in statistics.TotalsForAllResults) { Console.WriteLine(result.Key + " -> total:" + result.Value); } Console.WriteLine(); foreach (var row in statistics.ColumnHeaders) { Console.Write(row.Name + "\t"); } Console.WriteLine(); foreach (var row in statistics.Rows) { var rowItems = (List<string>)row; Console.WriteLine(rowItems.Aggregate((s1, s2) => s1 + "\t" + s2)); } Console.ReadLine(); } } }
چند نکته
ApplicationName همان نام پروژهای است که ابتدای کار، در گوگل ایجاد کردیم.
KeyFilePath مسیر فایل مجوز p12 ایی است که گوگل در حین ایجاد اکانت سرویس، در اختیار ما قرار میدهد.
ServiceAccountEmail آدرس ایمیل اکانت سرویس است که در قسمت ادمین Google Analytics به آن دسترسی دادیم.
SiteUrl آدرس سایت شما است که هم اکنون در Google Analytics دارای یک اکانت و پروفایل ثبت شدهاست.
توسط AnalyticsQueryParameters میتوان نحوهی کوئری گرفتن از Google Analytics را مشخص کرد. تاریخ شروع و پایان گزارش گیری در آن مشخص هستند. در مورد پارامترهایی مانند Dimensions و Metrics بهتر است به مرجع کامل آن در گوگل مراجعه نمائید:
Dimensions & Metrics Reference
برای نمونه در مثال فوق، تعداد کاربران، سشنهای آن و همچنین تعداد بار مشاهدهی صفحات، گزارشگیری میشود.
برای مطالعه بیشتر
Using Google APIs in Windows Store Apps
How To Use Google Analytics From C# With OAuth
Google Analytic’s API v3 with C#
.NET Library for Accessing and Querying Google Analytics V3 via Service Account
Google OAuth2 C#
برنامه IRIS PDF Editor
پی نوشت: نه تنها در یک تیم بلکه بهتر هست در یک کار انفرادی هم از این سیستمها استفاده کرد تا حداقل بازبینی روی پروژههای شخصی خود هم داشته باشیم.
پی نوشت: میتوانید از سیستمهای متن باز رایگان هم که قابل نصب بر روی هاست ها هم هستند استفاده کنید که در این حالت تنها هزینه هاست یا سرور برای شما میماند.
در سیستم گیت اصطلاحات زیادی وجود دارد: