مطالب
Google OpenID Authentication در ASP.NET با استفاده از DotNetOpenAuth
در پست‌های قبلی
در این پست می‌خواهیم با یک مثال ساده نشان دهیم که سایت ما چگونه با استفاده از OpenID Authentication می تواند از اکانت گوگل استفاده کرده و کاربر در وب سایت ما شناسایی شود.
برای اینکار ابتدا
1- کتابخانه DotNetOpenAuth را از طریق NuGet به لیست رفرنس‌های پروژه وب خود اضافه نمایید
2- یک صفحه بنام Login.aspx یا هر نام دلخواهی را به پروژه خود اضافه نمایید. در نهایت کد Html صفحه شما به ید به صورت زیر باشد.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="OAuthWebTest.Login" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div id="loginform">
        <div id="NotLoggedIn" runat="server">
            <asp:ImageButton ID="ButtonLoginWithGoogle" runat="server" ImageUrl="Google.JPG"
                OnCommand="ButtonLoginWithGoogle_Click" CommandArgument="https://www.google.com/accounts/o8/id" />
            <p />
            <asp:Label runat="server" ID="LabelMessage" />
        </div>
    </div>
    </form>
</body>
</html>
در کد Html بالا به خصوصیت CommandArgument از کنترل ImageButton دقت نمایید که دارای مقدار "https://www.google.com/accounts/o8/id" می باشد. در واقع این آدرس باید برای اکانت گوگل جهت احراز هویت توسط OpenID استفاده شود. (آدرس API گوگل برای استفاده از این سرویس)
3- در قسمت کد نویسی صفحه کد‌های زیر را وارد نمایید.

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using DotNetOpenAuth.OpenId.RelyingParty;

namespace OAuthWebTest
{
    public partial class Login : Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            OpenIdRelyingParty openIdRelyingParty = new OpenIdRelyingParty();
            var response = openIdRelyingParty.GetResponse();
            if (response == null) return;
            switch (response.Status)
            {
                case AuthenticationStatus.Authenticated:
                    NotLoggedIn.Visible = false;
                    Session["GoogleIdentifier"] = response.ClaimedIdentifier.ToString();
                    Response.Redirect("Default.aspx");
                    break;
                case AuthenticationStatus.Canceled:
                    LabelMessage.Text = "Cancelled.";
                    break;
                case AuthenticationStatus.Failed:
                    LabelMessage.Text = "Login Failed.";
                    break;
            }
        }

        protected void ButtonLoginWithGoogle_Click(object src, CommandEventArgs e)
        {
            string discoveryUri = e.CommandArgument.ToString();
            OpenIdRelyingParty openId = new OpenIdRelyingParty();
            var returnToUrl = new UriBuilder(Request.Url) { Query = "" };
            var request = openId.CreateRequest(discoveryUri, returnToUrl.Uri, returnToUrl.Uri);
            request.RedirectToProvider();
        }
    }
}
همانگونه که مشاهده می‌کنید  در رویداد کلیک دکمه لوگین ButtonLoginWithGoogle_Click ابتدا آدرس یکتا گوگل (مقدار CommandArgument اختصاص داده شده به کنترل ImageButton) به همراه آدرس صفحه جاری وب سایت توسط متد CreateRequest از شی openId ترکیب شده و یک درخواست (Request) ساخته شده و در نهایت برای سرویس دهنده گوگل ارسال می‌شود. با اجرای پروژه با تصویر زیر روبرو می‌شوید بروی کلید لوگین کلیک نمایید.

اجرای پروژه

در واقع با کلیک روی دکمه مورد نظر تکه کدی که در بالا شرح داده شد اجرا شده و ما را به صفحه ای شبیه تصویر پایین هدایت می‌کند

رفتن به صفحه گوگل جهت احراز هویت

در صورتی که کلید Sign In انتخاب شود شما به سایت (همین برنامه) اجازه داده اید که از اطلاعات حساب کاربری شما استفاده کند. پس از کلیک به برنامه اصلی (طبق آدرس بازگشت تعیین شده در رویداد ButtonLoginWithGoogle_Click) در رویداد PageLoad صفحه لاگین اطلاعات بازگشتی از سایت سرویس دهنده (در اینجا گوگل) چک می‌شود و با توجه با پاسخ مورد نظر عملیاتی انجام می‌شود، مثلا در صورتی که شما تایید کرده باشید اطلاعات شناسایی شما توسط گوگل در کلیدی به نام GoogleIdentifier در سشن ذهیره شده و شما به صفحه اصلی سایت هدایت می‌شوید.
دموی پروژه در این آدرس قرار داده شده است.
سورس پروژه از این آدرس قابل دسترسی است.
توجه: در این آدرس  شرح داده شده که چگونه دسترسی سایت‌های دیگر به اکانت گوگل خود را قطع کنید
در پست‌های آینده اتصال به تویتر و ف ی س بوک و سایت‌های دیگر توضیح داده خواهد شد.





مطالب
واکشی اطلاعات به صورت chunk chunk (تکه تکه) و نمایش در ListView
یکی از مسائلی که در هنگام کار با کنترلهای داده‌ای نظیر ListView , GridView و .. با آن روبرو هستیم مسئله صفحه بندی می‌باشد و در بسیاری از موارد، کل اطلاعات در هر درخواست، بارگذاری میشود. در حالیکه روش بهینه به این صورت است که با توجه به PageSize و Index رکورد، می‌توان تعداد رکورد مورد نظر در همان صفحه را بارگذاری کرد، نه کل رکوردها را.
در این مثال که از Ef Code First و الگوی Unit Of Work استفاده کرده ام ، قصد نمایش اطلاعات در یک ListView و صفحه بندی آن را بصورت chunk chunk دارم.
برای آشنایی بیشتر با الگوی Unit Of Work  می‌توانید به مقاله UOW در همین سایت مراجعه کنید.
   
ابتدا یک کنترل ListView روی صفحه ایجاد می‌کنیم. برای آشنایی بیشتر با این کنترل و بررسی قابلیتهای آن میتوان از این مقاله معرفی کنترل‌های ListView و DataPager استفاده کنید.

سپس با توجه به الگوی Unit Of Work از یک مدل ساده استفاده می‌کنیم. همچنین برای بارگذاری اطلاعات به صورت صفحه به صفحه، نیاز به داشتن Index رکورد و PageSize، جهت محاسبه تعداد رکورد مورد نیاز داریم.

public class User
    {       
        public Int64 UserId { get; set; }       
        public String UserName { get; set; }
     }        
 public interface IUserService
    {        
        int GetCustomerCount();
        List<User> GetCustomers(int StartIndex, int PageSize);
    }
public class ImplUserService : IUserService
    {
        IUnitOfWork _uow;
        IDbSet<User> _user;

        public ImplUserService(IUnitOfWork uow)
        {
            _uow = uow;
            _user = uow.Set<User>();
        }

        public int GetCustomerCount()
        {
            int totalCount = _user.ToList().Count;             
            return totalCount;
        }

        public List<User> GetCustomers(int StartIndex, int PageSize)
        {
            return _user.OrderBy(i => i.UserId).Skip(StartIndex).Take(PageSize).ToList();
        }
  }
در کدهای بالا متد GetCustomerCount تعداد کل رکوردهایی که باید واکشی شوند را مشخص می‌کند. همچنین می‌توان این تعداد را در cache یا viewState ذخیره کرد تا در دفعات بعدی در صورت خالی نبودن مقدار Cacheبا viewState ، نیاز به محاسبه مجدد count نداشته باشیم.

متد GetCustomer که کار اصلی را انجام میدهد از دو پارامتر استفاده می‌کند: StartIndex نقطه شروع و PageSize تعداد رکورد مورد نظر. در اینجا از دستورات Linq استفاده شده و دستور Skip مشخص میکند از کدام شماره رکورد به بعد شروع به واکشی نماید و دستور Take مشخص می‌کند که چه تعداد رکورد را واکشی نماید.

حالا به سراغ کدهای HTML می‌رویم. در آنجا علاوه بر ListView نیاز به DataPager جهت صفحه بندی و Object DataSource جهت کنترل بارگذاری اطلاعات به صورت chunk chunk داریم.
<asp:ListView ID="ListView1" runat="server" DataSourceID="ObjectDataSource1">
            <ItemTemplate>
                <tr style="background-color: #DCDCDC; color: #000000;">
                    <td>
                        <asp:Label ID="UserIdLabel" runat="server" Text='<%# Eval("UserId") %>' />
                    </td>
                    <td>
                        <asp:Label ID="UserNameLabel" runat="server" Text='<%# Eval("UserName") %>' />
                    </td>
                </tr>
            </ItemTemplate>
            
            
            <LayoutTemplate>
                <table runat="server">
                    <tr runat="server">
                        <td runat="server">
                            <table id="itemPlaceholderContainer" runat="server" border="1" style="background-color: #FFFFFF;
                                border-collapse: collapse; border-color: #999999; border-style: none; border-width: 1px;">
                                <tr runat="server" style="background-color: #DCDCDC; color: #000000;">
                                    <th runat="server">
                                        UserId
                                    </th>
                                    <th runat="server">
                                        UserName
                                    </th>
                                </tr>
                                <tr id="itemPlaceholder" runat="server">
                                </tr>
                            </table>
                        </td>
                    </tr>
                    <tr runat="server">
                        <td runat="server" style="text-align: center; background-color: #CCCCCC;
                            color: #000000;">
                            <asp:DataPager ID="DataPager1" runat="server" PageSize="2">
                                <Fields>
                                    <asp:NextPreviousPagerField ButtonType="Button" ShowFirstPageButton="True" ShowNextPageButton="False"
                                        ShowPreviousPageButton="False" />
                                    <asp:NumericPagerField />
                                    <asp:NextPreviousPagerField ButtonType="Button" ShowLastPageButton="True" ShowNextPageButton="False"
                                        ShowPreviousPageButton="False" />
                                </Fields>
                            </asp:DataPager>
                        </td>
                    </tr>
                </table>
            </LayoutTemplate>                        
        </asp:ListView>
همانظور که مشاهده میکنید در DataPager ، مقدار PageSize مشخص شده است.اما Object DataSource
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" EnablePaging="True" SelectCountMethod="GetCustomerCount"
            SelectMethod="GetCustomers" TypeName="UserService.ImplUserService" EnableViewState="False"
            MaximumRowsParameterName="PageSize" StartRowIndexParameterName="StartIndex">
که فکر می‌کنم با توجه به متدهای تعریف شده در بالا ، تعریف Object DataSource کاملاٌ گویا میباشد.

نکته مهم:
اگر الان برنامه را اجرا کنید با خطای No parameterless constructor defined for this object ر وبرو خواهید شد که جهت حل این مشکل از کد زیر استفاده میکنیم.
protected void ObjectDataSource1_ObjectCreating(object sender, ObjectDataSourceEventArgs e)
        {            
            e.ObjectInstance = ObjectFactory.GetInstance<IUserService>();            
        }
در واقع نیاز است تا یک وهله از کلاس مورد نظر را به Object DataSource معرفی کنیم.
حال با اجرای برنامه و Trace آن متوجه خواهید شد که با کلیک بر روی شماره صفحه، تنها به تعداد رکوردهای همان صفحه، واکشی خواهیم داشت.

با تشکر از راهنمایی‌های آقای نصیری، امیدوارم این مقاله برای دوستان مفید باشه. منتظر نظرات دوستان هستم و اینکه چه جوری بتونیم اینکار رو با jquery ajax و به صورت سبکتر انجام بدیم.

مطالب
اتصال Node.js به SQL Server با استفاده از Edge.js
اگر خواسته باشید که با استفاده از Node.js به SQL Server متصل شوید، احتمالا متوجه شده‌اید ماژولی که مایکروسافت منتشر کرده است، ناقص بوده و به صورت پیش نمایش است که بسیاری از ویژگی‌ها و مسائل مهم، در آن در نظر گرفته نشده است.

یکی دیگر از ماژول‌هایی که امکان اتصال Node.js را به SQL Server ممکن می‌کند، Edge.js است. Edge.js یک ماژول Node.js است که امکان اجرای کدهای دات نت را در همان پروسه توسط Node.js فراهم می‌کند. این مسئله، توسعه دهندگان Node.js را قادر می‌سازد تا از فناوری‌هایی که به صورت سنتی استفاده‌ی از آنها سخت یا غیر ممکن بوده است را به راحتی استفاده کنند. برای نمونه:
  • SQL Server
  • Active Directory
  • Nuget packages
  • استفاده از سخت افزار کامپیوتر (مانند وب کم، میکروفن و چاپگر)


نصب Node.js

اگر Node.js را بر روی سیستم خود نصب ندارید، می‌توانید از اینجا آن را دانلود کنید. بعد از نصب برای اطمینان از کارکرد آن، command prompt را باز کرده و دستور زیر را تایپ کنید:

node -v
شما باید نسخه‌ی نصب شده‌ی Node.js را مشاهده کنید.

ایجاد پوشه پروژه

سپس پوشه‌ای را برای پروژه Node.js خود ایجاد کنید. مثلا با استفاده از command prompt و دستور زیر:

md \projects\node-edge-test1
cd \projects\node-edge-test1

نصب Edge.js

Node با استفاده از package manager خود دانلود و نصب ماژول‌ها را خیلی آسان کرده است. برای نصب، در command prompt عبارت زیر را تایپ کنید:

npm install edge
npm install edge-sql
فرمان اول باعث نصب Edge.js و دومین فرمان سبب نصب پشتیبانی از SQL Server می‌شود.

Hello World

ایجاد یک فایل متنی با نام server.js و نوشتن کد زیر در آن:
var edge = require('edge');

// The text in edge.func() is C# code
var helloWorld = edge.func('async (input) => { return input.ToString(); }');

helloWorld('Hello World!', function (error, result) {
    if (error) throw error;
    console.log(result);
});
حالا برای اجرای این Node.js application از طریق command prompt کافی است به صورت زیر عمل کنید:
node server.js
همانطور که مشاهده می‌کنید "!Hello World" در خروجی چاپ شد.

ایجاد پایگاه داده تست

در مثال‌های بعدی، نیاز به یک پایگاه داده داریم تا query‌ها را اجرا کنیم. در صورتی که SQL Server بر روی سیستم شما نصب نیست، می‌توانید نسخه‌ی رایگان آن را از اینجا دانلود و نصب کنید. همچنین SQL Management Studio Express را نیز نصب کنید.

  1. در SQL Management Studio، یک پایگاه داده را با نام node-test با تنظیمات پیش فرض ایجاد کنید.
  2. بر روی پایگاه داده node-test راست کلیک کرده و New Query را انتخاب کنید.
  3. اسکریپت زیر را copy کرده و در آنجا paste کنید، سپس بر روی Execute کلیک کنید.
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('SampleUsers')) BEGIN; DROP TABLE SampleUsers; END; GO

CREATE TABLE SampleUsers ( Id INTEGER NOT NULL IDENTITY(1, 1), FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Email VARCHAR(255) NOT NULL, CreateDate DATETIME NOT NULL DEFAULT(getdate()), PRIMARY KEY (Id) ); GO

INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Orla','Sweeney','nunc@convallisincursus.ca','Apr 13, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Zia','Pickett','porttitor.tellus.non@Duis.com','Aug 31, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Justina','Ayala','neque.tellus.imperdiet@temporestac.com','Jul 28, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Levi','Parrish','adipiscing.elit@velarcueu.com','Jun 21, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Pearl','Warren','In@dignissimpharetra.org','Mar 3, 2014');
نتیجه‌ی اجرای کد بالا، ایجاد جدولی با نام SampleUsers و درج 5 رکورد در آن می‌شود.

تنظیمات ConnectionString

قبل از استفاده از Edge.js با SQL Server، باید متغیر محیطی (environment variable) با نام EDGE_SQL_CONNECTION_STRING را تعریف کنید.

set EDGE_SQL_CONNECTION_STRING=Data Source=localhost;Initial Catalog=node-test;Integrated Security=True
این متغیر تنها برای command prompt جاری تعریف شده است و با بستن آن از دست می‌رود. در صورتیکه از Node.js Tools for Visual Studio استفاده می‌کنید، نیاز به ایجاد یک متغیر محیطی دائمی و راه اندازی مجدد VS دارید. همچنین در صورتیکه بخواهید متغیر محیطی دائمی ایجاد کنید، فرمان زیر را اجرا کنید:
SETX EDGE_SQL_CONNECTION_STRING "Data Source=localhost;Initial Catalog=node-test;Integrated Security=True"


روش اول: اجرای مستقیم SQL Server Query در Edge.js

فایلی با نام server-sql-query.js را ایجاد کرده و کد زیر را در آن وارد کنید:

var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

var getTopUsers = edge.func('sql', function () {/*
    SELECT TOP 3 * FROM SampleUsers ORDER BY CreateDate DESC
*/});

function logError(err, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.write("Error: " + err);
    res.end("");
}    

http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html' });

    getTopUsers(null, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
        }
    });
}).listen(port);
console.log("Node server listening on port " + port);
سپس با استفاده از command prompt، فرمان زیر را اجرا کنید:
node server-sql-query.js
حال مرورگر خود را باز و سپس آدرس http://localhost:8080 را باز کنید. در صورتی که همه چیز به درستی انجام گرفته باشد لیستی از 3 کاربر را خواهید دید.

روش دوم: اجرای کد دات نت برای SQL Server Query

Edge.js تنها از دستورات Update، Insert، Select و Delete پشتیبانی می‌کند. در حال حاضر از store procedures و مجموعه‌ای از کد SQL پشتیبانی نمی‌کند. بنابراین، اگر چیزی بیشتر از عملیات CRUD می‌خواهید انجام دهید، باید از دات نت برای این کار استفاده کنید.

یادتان باشد، همیشه async

مدل اجرایی Node.js به صورت یک حلقه‌ی رویداد تک نخی است. بنابراین این بسیار مهم است که کد دات نت شما به صورت async باشد. در غیر اینصورت یک فراخوانی به دات نت سبب مسدود شدن و ایجاد خرابی در Node.js می‌شود.

ایجاد یک Class Library

اولین قدم، ایجاد یک پروژه Class Library در Visual Studio که خروجی آن یک فایل DLL است و استفاده از آن در Edge.js است. پروژه Class Library با عنوان EdgeSampleLibrary ایجاد کرده و فایل کلاسی با نام Sample1 را به آن اضافه کنید و سپس کد زیر را در آن وارد کنید:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace EdgeSampleLibrary
{
     public class Sample1
    {
        public async Task<object> Invoke(object input)
        {
            // Edge marshalls data to .NET using an IDictionary<string, object>
            var payload = (IDictionary<string, object>) input;
            var pageNumber = (int) payload["pageNumber"];
            var pageSize = (int) payload["pageSize"];
            return await QueryUsers(pageNumber, pageSize);
        }

        public async Task<List<SampleUser>> QueryUsers(int pageNumber, int pageSize)
        {
            // Use the same connection string env variable
            var connectionString = Environment.GetEnvironmentVariable("EDGE_SQL_CONNECTION_STRING");
            if (connectionString == null)
                throw new ArgumentException("You must set the EDGE_SQL_CONNECTION_STRING environment variable.");

            // Paging the result set using a common table expression (CTE).
            // You may rather do this in a stored procedure or use an 
            // ORM that supports async.
            var sql = @"
DECLARE @RowStart int, @RowEnd int;
SET @RowStart = (@PageNumber - 1) * @PageSize + 1;
SET @RowEnd = @PageNumber * @PageSize;

WITH Paging AS
(
    SELECT  ROW_NUMBER() OVER (ORDER BY CreateDate DESC) AS RowNum,
            Id, FirstName, LastName, Email, CreateDate
    FROM    SampleUsers
)
SELECT  Id, FirstName, LastName, Email, CreateDate
FROM    Paging
WHERE   RowNum BETWEEN @RowStart AND @RowEnd
ORDER BY RowNum;
";
            var users = new List<SampleUser>();

            using (var cnx = new SqlConnection(connectionString))
            {
                using (var cmd = new SqlCommand(sql, cnx))
                {
                    await cnx.OpenAsync();

                    cmd.Parameters.Add(new SqlParameter("@PageNumber", SqlDbType.Int) { Value = pageNumber });
                    cmd.Parameters.Add(new SqlParameter("@PageSize", SqlDbType.Int) { Value = pageSize });

                    using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection))
                    {
                        while (await reader.ReadAsync())
                        {
                            var user = new SampleUser
                            {
                                Id = reader.GetInt32(0), 
                                FirstName = reader.GetString(1), 
                                LastName = reader.GetString(2), 
                                Email = reader.GetString(3), 
                                CreateDate = reader.GetDateTime(4)
                            };
                           users.Add(user);
                        }
                    }
                }
            }
            return users;
        } 
    }

    public class SampleUser
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public DateTime CreateDate { get; set; }
    }
}
سپس ذخیره و کامپایل کنید. فایل DLL خروجی که در مسیر
[project]/bin/Debug/EdgeSampleLibrary.dll
قرار دارد را در پوشه‌ی پروژه Node کپی کنید. فایل جدیدی را با نام server-dotnet-query.js در پروژه Node ایجاد کنید و کد زیر را در آن وارد کنید:
var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

// Set up the assembly to call from Node.js 
var querySample = edge.func({ assemblyFile: 'EdgeSampleLibrary.dll', typeName: 'EdgeSampleLibrary.Sample1', methodName: 'Invoke' });

function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Got error: " + err); res.end(""); }

http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' });

    // This is the data we will pass to .NET
    var data = { pageNumber: 1, pageSize: 3 };

    // Invoke the .NET function
    querySample(data, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
            res.end("No results");
        }
    });

}).listen(port);

console.log("Node server listening on port " + port);
سپس از طریق command prompt آن را اجرا کنید:
node server-dotnet-query.js
حال مرورگر خود را باز کرده و به آدرس http://localhost:8080 بروید. در صورتیکه همه چیز به درستی انجام گرفته باشد، لیستی از 3 کاربر را خواهید دید. مقادیر pageNumber و pageSize را در فایل جاوااسکریپت تغییر دهید و تاثیر آن را بر روی خروجی مشاهده کنید.
 
نکته: برای ایجاد pageNumber و pageSize داینامیک با استفاده از ارسال مقادیر توسط QueryString، می‌توانید از ماژول connect استفاده کنید.
مطالب
ساخت یک Form Generator ساده در MVC
در ادامه می‌خواهیم نحوه‌ی ایجاد یک فرم‌ساز ساده را ASP.NET MVC بررسی کنیم.
مدل‌های برنامه ما به صورت زیر می‌باشند:
namespace SimpleFormGenerator.DomainClasses
{
    public class Form
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public virtual ICollection<Field> Fields { get; set; }
    }
    public class Field
    {
        public int Id { get; set; }
        public string TitleEn { get; set; }
        public string TitleFa { get; set; }
        public FieldType FieldType { get; set; }
        public virtual Form Form { get; set; }
        public int FormId { get; set; }

    }
    public enum FieldType
    {
        Button,
        Checkbox,
        File,
        Hidden,
        Image,
        Password,
        Radio,
        Reset,
        Submit,
        Text
    }
    
}
توضیح مدل‌های فوق:
همانطور که مشاهده می‌کنید برنامه ما از سه مدل تشکیل شده است. اولین مورد آن کلاس فرم است. این کلاس در واقع بیانگر یک فرم است که در ساده‌ترین حالت خود از یک Id، یک عنوان و تعدادی از فیلدها تشکیل می‌شود. کلاس فیلد نیز بیانگر یک فیلد است که شامل: آی‌دی، عنوان انگلیسی فیلد، عنوان فارسی فیلد، نوع فیلد (که در اینجا از نوع enum انتخاب شده است که خود شامل چندین آیتم مانند Text, Radioو... است) و کلید خارجی کلاس فرم می‌باشد. تا اینجا مشخص شد که رابطه فرم با فیلد، یک رابطه یک به چند است؛ یعنی یک فرم می‌تواند چندین فیلد داشته باشد.
کلاس کانتکست برنامه نیز به این صورت می‌باشد:
namespace SimpleFormGenerator.DataLayer.Context
{
    public class SimpleFormGeneratorContext : DbContext, IUnitOfWork
    {
        public SimpleFormGeneratorContext()
            : base("SimpleFormGenerator") {}
        public DbSet<Form> Forms { get; set; }
        public DbSet<Field> Fields { get; set; }
        public DbSet<Value> Values { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Value>()
    .HasRequired(d => d.Form)
    .WithMany()
    .HasForeignKey(d => d.FormId)
    .WillCascadeOnDelete(false);

        }
        
    }
}
 همانطور که مشاهده می‌کنید مدل‌های برنامه را در معرض دید EF قرار داده‌ایم. تنها نکته‌ایی که در کلاس فوق مهم است متد OnModelCreating است. از آنجائیکه رابطه کلاس Field و Value یک رابطه یک‌به‌یک است باید ابتدا و انتهای روابط را برای این دو کلاس تعیین کنیم.
 
 تا اینجا می‌توانیم به کاربر امکان ایجاد یک فرم و همچنین تعیین فیلد‌های یک فرم را بدهیم. برای اینکار ویو‌های زیر را در نظر بگیرید:
ویو ایجاد یک فرم:
@model SimpleFormGenerator.DomainClasses.Form

@{
    ViewBag.Title = "صفحه ایجاد یک فرم";
}


@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div>
            <span>عنوان</span>
            <div>
                @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
            </div>
        </div>

        
        <div>
            <div>
                <input type="submit" value="ذخیره" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("بازگشت", "Index")
</div>
ویوی ایجاد فیلد برای هر فرم:
@model SimpleFormGenerator.DomainClasses.Field

@{
    ViewBag.Title = "CreateField";
}

@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div>
            <span>عنوان انگلیسی</span>
            <div>
                @Html.EditorFor(model => model.TitleEn, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.TitleEn, "", new { @class = "text-danger" })
            </div>
        </div>

        <div>
            <span>عنوان فارسی</span>
            <div>
                @Html.EditorFor(model => model.TitleFa, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.TitleFa, "", new { @class = "text-danger" })
            </div>
        </div>

        <div>
            <span>نوع فیلد</span>
            <div>
                @Html.EnumDropDownListFor(model => model.FieldType, htmlAttributes: new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.FieldType, "", new { @class = "text-danger" })
            </div>
        </div>

        <div>
            <span>فرم</span>
            <div>
                @Html.DropDownList("FormId", (SelectList)ViewBag.FormList)
                @Html.ValidationMessageFor(model => model.FormId, "", new { @class = "text-danger" })
            </div>
        </div>

        <div>
            <div>
                <input type="submit" value="ذخیره" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("بازگشت ", "Index")
</div>
در ویوی فوق کاربر می‌تواند برای فرم انتخاب شده فیلدهای موردنظر را تعریف کند:


ویوی نمایش فرم تولید شده برای کاربر نهایی:
@using SimpleFormGenerator.DomainClasses
@model IEnumerable<SimpleFormGenerator.DomainClasses.Field>

@{
    ViewBag.Title = "نمایش فرم";
}

<div>
    <div>
        <div>
            @using (Html.BeginForm())
            {
                @Html.AntiForgeryToken()
                for (int i = 0; i < Model.Count(); i++)
                {
                    if (Model.ElementAt(i).FieldType == FieldType.Text)
                    {
                        <text>
                            <input type="hidden" name="[@i].FieldType" value="@Model.ElementAt(i).FieldType" />
                            <input type="hidden" name="[@i].Id" value="@Model.ElementAt(i).Id" /> 
                            <input type="hidden" name="[@i].FormId" value="@Model.ElementAt(i).FormId" /> 
                            <div>
                                <label>@Model.ElementAt(i).TitleFa</label>
                                <div>
                                    <input type="text" name="[@i].TitleEn" />
                                </div>
                            </div>

                        </text>

                    }
                }
                <div data-formId ="@ViewBag.FormId">
                    <div>
                        <input type="submit" value="ارسال فرم" />
                    </div>
                </div>
            }
        </div>
        <div>
            @Html.ActionLink("بازگشت", "Index")
        </div>
    </div>
</div>
همانطور که در کدهای فوق مشخص است از اکشن متدی که در ادامه مشاهده خواهید کرد لیستی از فیلدهای مربوط به یک فرم را برای کاربر به صورت رندر شده نمایش داده‌ایم. در اینجا باید براساس فیلد FieldType، نوع فیلد را تشخیص دهیم و المنت متناسب با آن را برای کاربر نهایی رندر کنیم. برای اینکار توسط یک حلقه for در بین تمام فیلدها پیمایش می‌کنیم:
for (int i = 0; i < Model.Count(); i++)
{
     // code
}
سپس در داخل حلقه یک شرط را برای بررسی نوع فیلد قرار داده‌ایم:
if (Model.ElementAt(i).FieldType == FieldType.Text)
{
     // code
}
بعد از بررسی نوع فیلد، خروجی رندر شده به این صورت برای کاربر نهایی به صورت یک عنصر HTML نمایش داده می‌شود:
<input type="text" name="[@i].TitleEn" />
همانطور که در کدهای قبلی مشاهده می‌کنید یکسری فیلد را به صورت مخفی بر روی فرم قرار داده‌ایم زیرا در زمان پست این اطلاعات به سرور از آنجائیکه مقادیر فیلدهای فرم تولید شده ممکن است چندین مورد باشند، به صورت آرایه‌ایی از عناصر آنها را نمایش خواهیم داد:
[@i].FieldTyp
خوب، تا اینجا توانستیم یک فرم‌ساز ساده ایجاد کنیم. اما برای ارسال این اطلاعات به سرور به یک مدل دیگر احتیاج داریم. این جدول در واقع محل ذخیره‌سازی مقادیر فیلدهای یک فرم و یا فرم‌های مختلف است. 
public class Value
{
        public int Id { get; set; }
        public string Val { get; set; }
        public virtual Field Field { get; set; }
        [ForeignKey("Field")]
        public int FieldId { get; set; }
        public virtual Form Form { get; set; }
        [ForeignKey("Form")]
        public int FormId { get; set; }
        
}
این جدول در واقع شامل: آی‌دی، مقدار فیلد، کلید خارجی فیلد و کلید خارجی فرم می‌باشد. بنابراین برای ارسال ویو قبلی به سرور اکشن‌متد ShowForm را در حالت Post به این صورت خواهیم نوشت:
[HttpPost]
        public ActionResult ShowForm(IEnumerable<Field> values)
        {

            if (ModelState.IsValid)
            {
                foreach (var value in values)
                {
                    _valueService.AddValue(new Value { Val = value.TitleEn, FormId = value.FormId, FieldId = value.Id});
                    _uow.SaveAllChanges();
                }
            }
            return View(values);
        }
سورس مثال جاری را نیز می‌توانید از اینجا دریافت کنید.
نظرات مطالب
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
یک نکته‌ی تکمیلی: همیشه بهتر است در سمت کلاینت هم تاریخ انقضای JWT پیش از استفاده بررسی شود
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;

namespace BlazorWasm.Client.Utils
{
    public class JwtInfo
    {
        public IEnumerable<Claim> Claims { set; get; }

        public DateTime? ExpirationDateUtc { set; get; }

        public bool IsExpired { set; get; }

        public IEnumerable<string> Roles { set; get; }
    }

    /// <summary>
    /// From the Steve Sanderson’s Mission Control project:
    /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
    /// </summary>
    public static class JwtParser
    {
        public static JwtInfo ParseClaimsFromJwt(string jwt)
        {
            var claims = new List<Claim>();
            var payload = jwt.Split('.')[1];

            var jsonBytes = getBase64WithoutPadding(payload);

            foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes))
            {
                if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
                {
                    foreach (var itemValue in element.EnumerateArray())
                    {
                        claims.Add(new Claim(keyValue.Key, itemValue.ToString()));
                    }
                }
                else
                {
                    claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString()));
                }
            }

            var roles = getRoles(claims);
            var expirationDateUtc = getDateUtc(claims, "exp");
            var isExpired = getIsExpired(expirationDateUtc);
            return new JwtInfo
            {
                Claims = claims,
                Roles = roles,
                ExpirationDateUtc = expirationDateUtc,
                IsExpired = isExpired
            };
        }

        private static IList<string> getRoles(IList<Claim> claims) =>
            claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();

        private static byte[] getBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }

        private static bool getIsExpired(DateTime? expirationDateUtc) =>
            !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow);

        private static DateTime? getDateUtc(IList<Claim> claims, string type)
        {
            var exp = claims.SingleOrDefault(claim => claim.Type == type);
            if (exp == null)
            {
                return null;
            }

            var expValue = getTimeValue(exp.Value);
            if (expValue == null)
            {
                return null;
            }

            var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            return dateTimeEpoch.AddSeconds(expValue.Value);
        }

        private static long? getTimeValue(string claimValue)
        {
            if (long.TryParse(claimValue, out long resultLong))
                return resultLong;

            if (float.TryParse(claimValue, out float resultFloat))
                return (long)resultFloat;

            if (double.TryParse(claimValue, out double resultDouble))
                return (long)resultDouble;

            return null;
        }
    }
}
نظرات مطالب
نمایش خطاهای اعتبارسنجی سمت سرور ASP.NET Core در برنامه‌های Angular
نکته تکمیلی
با کمی تغییرات در بدنه متد processModelStateErrors، می‌توان برای سناریوهای master-detail که خطاهایی به شکل زیر از سمت سرور دریافت می‌شود نیز پیغام متناظر به کنترل‌های موجود در FormArray‌ها را نیز به درستی نمایش داد:
{
  'detail[0].title':['message'],
  'detail[0].detail[0].title':['message']
}
برای این منظور متد findFieldControl را به شکل زیر پیاده سازی خواهیم کرد:
  private findFieldControl(fieldName: string): AbstractControl {
    const path = fieldName.replace('[', '.').replace(']', '');
    return (
      this.form.get(path) || this.form.get(this.lowerCaseFirstLetter(path))
    );
  }
متد get استفاده شده در بدنه متد، امکان دریافت path متناظر با یک کنترل را نیز دارد؛ از این جهت صرفا لازم بود کلید دریافتی از سمت سرور را به شکل زیر تبدیل کنیم:
'detail.0.title'
'detail.0.detail.0.title'
و در نهایت اعمال تغییرات به متد processModelStateErrors به شکل زیر می‌باشد:
protected handleSubmitError(response: HttpErrorResponse) {
    this.messages = [];

    if (response.status === 400) {
      // handle validation errors
      const validationDictionary = response.error;
      for (const fieldName in validationDictionary) {
        if (validationDictionary.hasOwnProperty(fieldName)) {
          const messages = validationDictionary[fieldName];
          const control = this.findFieldControl(fieldName);
          if (control) {
            // integrate into angular's validation if we have field validation
            control.setErrors({
              model: { messages: messages }
            });
          } else {
            // if we have cross field validation, then show the validation error at the top of the screen
            this.messages.push(...messages);
          }
        }
      }
    } else {
      this.messages.push('something went wrong!');
    }
  }

بازخوردهای پروژه‌ها
تشخیص بات یا سایر خزنده‌ها
ضمن تشکر از شما بابت به اشتراک گذاشتن پروژه، به نظرم تنها استفاده از context.Request.Browser.Crawler برای تشخیص اینکه آیا درخواست رسیده، خزنده هست یا خیر، کافی نیست. شاید بتوان در کنار و علاوه بر این کنترل، یک شرط دیگر هم به نحو زیر اضافه کرد و در نظر گرفت:
private static readonly List<string> KnownCrawlers = new List<string>
{
    "bot","crawler","baiduspider","80legs","ia_archiver","ahrefsBot","twitterbot",
    "yoozbot","yandexBot","bitlybot","other", "sogou web spider", "python requests",
    "voyager","curl","wget","yahoo! slurp","mediapartners-google", "mj12bot",
    "seznamBot", "Sogou web spider", "360Spider", "sogouwebspider"
};
public static bool IsBotOrCrawler(string agent)
{
    agent = agent.ToLower();
    return KnownCrawlers.Any(crawler => agent.Contains(crawler) || agent.Equals(crawler));
}
می‌توان به مرور، لیست خزنده‌ها را تکمیل کرد.