LINQ 和 WF 基于 ASP.NET 3.5 的自定义配置文件提供程序






4.75/5 (26投票s)
ASP.NET 3.5 的自定义配置文件提供程序的新实现,使用了 LINQ、工作流基础和面向责任的单例 DataContexts 模式。
目录
概述
配置文件提供程序是 ASP.NET 2.0 版本中最突出的功能之一;开发人员可以使用它来代替传统的状态管理选项来存储用户特定数据。当然,配置文件提供程序的与众不同之处在于,用户特定信息会被永久持久化并存储在后端数据存储中,在几乎所有情况下,出于默示原因,这个数据存储是一个 SQL Server 数据库。
默认配置文件的最常见限制并非出乎意料的性能,而实际上在于数据持久化的实现方式。一旦数据持久化到数据存储中,它只能以三种格式之一进行序列化:字符串、XML 或二进制;之后,它将被塞入一个字段。毋庸置疑,在这些限制下编写自定义解析逻辑是一场噩梦。
与 .NET 框架中的许多功能一样,可扩展性通常是可选项。该系统允许自定义配置文件提供程序;事实上,一位来自 Microsoft 的软件工程师 Hao Kung 曾为此提供了帮助,他编写了一个 自定义配置文件提供程序 [^],支持基于表和基于存储过程的方法。这是一段非常受欢迎的代码,启发了我们深入研究 ASP.NET 配置文件提供程序的细节和内部工作原理。
编写基于 LINQ 的自定义配置文件感觉像是对配置文件功能的一种非常自然的扩展。我确信这个话题已经存在于许多开发者的脑海中;因此,我将尽我最大的努力写一篇有用的文章,希望对那些希望在 .NET 3.5 环境中利用配置文件提供程序的其他开发者有所帮助。
本文还介绍了 Windows 工作流基础;我忍不住使用 WF 来实现自定义配置文件提供程序所需的业务逻辑。这增加了一个全新的趣味性,也为从 ASP.NET Web 应用程序中使用 WF 运行时提供了一个独立的研究机会。
基础
本节将涵盖每个领域(网站、LINQ to SQL 和 WF)所需的设计和实现步骤。我将演示如何设计这些构建块,然后在后面的章节中,我将把它们全部融合在一起。
配置 Web 应用程序
毫无疑问,web.config 有时是 Web 开发者的好朋友。我知道对我来说是这样,除了那些 Visual Studio 2005 或 2008 动态编译 web.config 中标记内容时出现问题的时候。回到正题,这是一段推荐配置的摘录
<system.web>
<profile enabled="true"
automaticSaveEnabled="false"
defaultProvider="CPDemoUserProfileProvider"
inherits="BusinessLogic.BusinessObjects.CPDemoUserProfile">
<providers>
<clear/>
<!--
The SqlProvider is added here to demonstrate that
it is possible to
declare and use multiple profile Providers at once.
-->
<add name="SqlProvider" type="System.Web.Profile.SqlProfileProvider"
connectionStringName="MyConnectionString"
applicationName="CPDemo"/>
<add name="CPDemoUserProfileProvider"
type="BusinessLogic.Providers.CPDemoUserProfileProvider"
ApplicationName="CPDemo"
ApplicationGUID="DB487110-2809-42F0-BDA0-742C088C75F3"/>
</providers>
</profile>
</system.web>
您可能已经注意到 <properties></properties>
标签的缺失,但在我们完成下一步编写自定义配置文件对象时,这一点将变得清晰。
关于 ApplicationGUID
属性的几句话。随 SQL Server 2005 提供的配置文件提供程序数据库可以容纳单个数据库中的多个应用程序。从性能角度来看,这可能不是理想的;因此,为了最大限度地减少数据库架构更改,使用了硬编码的应用程序标识符值。有关配置文件表架构的更多信息,请参阅 Microsoft 文档。
构建自定义配置文件
为了本次演示的目的,我们将假设一个用户配置文件包含通常的个人信息(姓名、地址等)以及一个对应于成功信贷申请的数字以及批准日期。从结构上看,自定义配置文件对象看起来是这样的

现在,进入用户配置文件的实际代码。有几种方法可以生成用户配置文件的强类型版本
- Visual Studio 会根据 web.config 中的配置文件属性自动生成强类型对象
- 第三方自定义工具也可以生成具有附加属性的强类型 Web 配置文件
- 老式方法:手动编码
如果您的配置文件中没有复杂的数据类型和 PropertyGroups,手动编码自定义配置文件是相当容易和直接的。对于本次演示,这是自定义配置文件对象的实际代码(为简洁起见,此处仅显示少数属性)
[Serializable]
public class CPDemoUserProfile : ProfileBase
{
[SettingsAllowAnonymous(true)]
[DefaultSettingValue("1/1/0001 12:00:00 AM")]
public virtual System.DateTime LastApprovedDate
{
get
{
return ((System.DateTime)(this.GetPropertyValue("LastApprovedDate")));
}
set
{
this.SetPropertyValue("LastApprovedDate", value);
}
}
[SettingsAllowAnonymous(true)]
[DefaultSettingValue("LightBlue")]
public string Theme
{
get
{
return ((string)(this.GetPropertyValue("Theme")));
}
set
{
this.SetPropertyValue("Theme", value);
}
}
[SettingsAllowAnonymous(true)]
public virtual string State
{
get
{
return ((string)(this.GetPropertyValue("State")));
}
set
{
this.SetPropertyValue("State", value);
}
}
[SettingsAllowAnonymous(true)]
[DefaultSettingValue("1/1/0001 12:00:00 AM")]
public System.DateTime LastProfileUpdateDate
{
get
{
return ((System.DateTime)(this.GetPropertyValue
("LastProfileUpdateDate")));
}
}
[SettingsAllowAnonymous(true)]
public virtual string LastName
{
get
{
return ((string)(this.GetPropertyValue("LastName")));
}
set
{
this.SetPropertyValue("LastName", value);
}
}
}
通常,当配置了配置文件提供程序时,Visual Studio 会尝试编译所讨论的配置文件的强类型版本。在常用默认提供程序的场景中,ProfileCommon
类允许我们访问用户配置文件的属性。添加 inherits
指令将强制 ProfileCommon
继承我们自己的自定义配置文件类。这将主要以积极的方式影响 ASPX 网页中的编码,以便我们可以直接使用 Page.Profile
属性,而无需转换为我们自定义用户配置文件对象的类型。为了更好地说明这一点,一旦 web.config 被编译,Page
类的 ProfileCommon
属性的定义将是
public class ProfileCommon : BusinessLogic.BusinessObjects.CPDemoUserProfile {
public virtual ProfileCommon GetProfile(string username) {
return ((ProfileCommon)(ProfileBase.Create(username)));
}
}
原型化 ProfileProvider 类
遵循 Microsoft 采纳和实现的 提供程序模型设计模式和规范 [^],我们可以通过继承某些基类来构建自己的提供程序。对于自定义配置文件提供程序,相关的基类是 ProfileProvider
。对于本文而言,只重写和实现下图中所示的方法。

在接下来的几节中,我们将填补空白。但目前,这是我们将要处理的内容的骨架代码大纲
public class CPDemoUserProfileProvider : ProfileProvider
{
private string _appName = String.Empty;
private Guid _appGuid = Guid.Empty;
private string _providerName = String.Empty;
public override void Initialize(string name, NameValueCollection config)
{
_appGuid = new Guid(config["ApplicationGUID"]);
_appName = config["ApplicationName"];
_providerName = name;
base.Initialize(name, config);
}
public override string Name
{
get { return _providerName; }
}
public override string ApplicationName
{
get { return _appName; }
set { return; }
}
public override SettingsPropertyValueCollection GetPropertyValues
(SettingsContext context, SettingsPropertyCollection collection)
{
throw new NotImplementedException();
}
public override void SetPropertyValues
(SettingsContext context, SettingsPropertyValueCollection collection)
{
throw new NotImplementedException();
}
public override int DeleteInactiveProfiles
(ProfileAuthenticationOption authenticationOption,
DateTime userInactiveSinceDate)
{
throw new NotImplementedException();
}
/*
* Custom ProfileManger methods
*/
public int GetTotalNumberofProfiles()
{
throw new NotImplementedException();
}
}
LINQ to SQL DataContext
快速回顾一下,默认的 Microsoft 配置文件架构可以使用 aspnet_regsql.exe 工具生成,该工具通常位于 Windows\Microsoft.NET\Framework\v2.0.50727 目录中,通过运行以下命令...
aspnet_regsql.exe -S 'server name' -d 'database name' -A p -E
...这将安装随默认配置文件提供程序捆绑的所有表、视图和存储过程。但是,我们只对两个表感兴趣:aspnet_Users 和 aspnet_Application。正如您将看到的,我们将放弃使用任何提供的存储过程。用于持久化用户配置文件的默认表名为 aspnet_Profile,但正如我提到的,我们的目标是创建并使用自己的表来存储自定义配置文件数据。
当然,您也可以在不带参数的情况下运行 aspnet_regsql.exe 命令来启动 GUI 向导。
基于 CPDemoUserProfile
对象,我们对存储自定义配置文件数据所需的表应该是什么样子有了很好的了解。我更像是一个 C# 人而不是数据库人,我选择从顶部到底部的方法来设计我的自定义配置文件,即从业务逻辑层(或业务域)内部开始设计。其他人可能更倾向于从底部到顶部,如果他们是更面向数据库的开发人员和架构师。无论如何,我们的自定义配置文件表(也命名为 CPDemoUserProfile)以及我们需要的两个表,在将它们导入 Visual Studio 2008 的 LINQ to SQL 文档后看起来是这样的

名为 ApprovalNumberHistory 的附加表是为了演示扩展配置文件功能不必受限制。毕竟,我们对所有后续数据库操作都有最终控制权。
在构建包含 LINQ to SQL 文件的应用程序后,Visual Studio 会自动生成相应的 DataContext
对象,其方法和属性映射到我们上面选择的数据库架构。这把我带到了架构中的下一步。现在,下一步纯粹基于个人设计和架构偏好,无论您选择采用什么其他设计和实现方法,都不会影响本文的核心思想。
DataContext 作为单例
我完全意识到我正在 tackling 一个相当有争议的问题。许多人会争论使用 Singleton DataContext
s 的优点和缺点,以及在调用代码的范围内未能正确处置 DataContext
的后果。此外,在我的设计策略中,我考虑了以下因素
- 如果通过
DataContext
对象意外访问了通过DataContext
检索到的数据,则不正确地处置DataContext
对象将导致ObjectDisposed
异常。 - 不强制要求显式处置
DataContext
对象(通过调用Dispose()
方法)。 DataContext
不持有到数据库的任何打开连接,而是使用连接池中当前可用的连接。一旦DataContext
对象完成了其数据操作,连接就会返回到池中。有关更深入的讨论,请查看 Scott Guthrie 的这篇博文 [^]。
功能特定的单例模式
除了上述因素,我个人更喜欢拥有多个单例,每个单例负责一个原子逻辑单元,而不是使用一个处理多种职责的庞大对象。举个具体的例子;在我当前正在进行的一个真实项目中(这启发我写这篇文章),后端不仅包含自定义配置文件架构,还包含许多与用户管理、财务和其他领域特定实体相关的其他架构。为了一个更易于管理的解决方案,我决定拥有几个 DataContext
单例,每个单例专用于数据库中的一个独立域单元。这解释了为什么本文的 DataContext
只包含与自定义配置文件功能相关的表和实体。由此产生的 DataContext
将全权负责配置文件相关的操作。
在介绍了这些内容之后,我可以展示基于前面介绍的 LINQ to SQL 架构的 ProfileDataContext
。

既然我已经确定了多个 DataContext
单例设计模式的前提,我可以介绍一个管理这些单例创建的小型实用程序类。DataContextManager<T>
类具有以下骨架
public static class DataContextManager<T> where T : DataContext, new()
{
private static T _context = null;
static DataContextManager()
{
lock (typeof(DataContextManager<T>))
{
if (null == _context)
{
_context = new T();
}
}
}
public static T GetInstance()
{
return (null == _context) ? new T() : _context;
}
public static void UpdateEntity<K>(K entity, Action<K> updateAction)
where K : class
{
T instance = GetInstance();
//As a precaution attach entity to a DataContext before update.
//Context might be lost when passing through application boundaries.
instance.GetTable<K>().Attach(entity, true);
updateAction(entity);
instance.SubmitChanges();
}
public static void DeleteEntity<K>(K entity) where K : class
{
T instance = GetInstance();
Table<K> table = instance.GetTable<K>();
table.Attach(entity);
table.DeleteOnSubmit(entity);
instance.SubmitChanges();
}
}
下面是一个演示如何使用 DataContextManager<T>
的示例
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
IQueryable<aspnet_User> users = db.CPDemoUserProfiles.Select
(p => p.aspnet_User).AsQueryable();
扩展 DataContext 分部类
我欣赏拥有功能特定的 DataContext
对象还有一点,就是通过使用分部类功能,我们可以扩展自动生成的 DataContext
类,并添加我们自己的自定义加载逻辑和其他属性验证逻辑。我在此演示如何通过更改 DataContext
的加载选项来优化 LINQ 引擎发出的 SQL 语句
partial class ProfileDataContext
{
private StreamWriter _log;
partial void OnCreated()
{
//LoadOptions
DataLoadOptions options = new DataLoadOptions();
options.LoadWith<CPDemoUserProfile>(p => p.aspnet_User);
this.LoadOptions = options;
/*
* It is always a good idea to keep an eye on what SQL statements
* are emitted by LINQ engine
* this could be a good practice during development phase
* so that appropriate database optimization
* measures are taken (indexes, query optimization, etc...).
*/
_log = new StreamWriter(@"C:\Logs\ProfileDataContext.txt",
false, System.Text.Encoding.ASCII);
_log.AutoFlush = true;
this.Log = _log;
}
protected override void Dispose(bool disposing)
{
_log.Close();
_log.Dispose();
base.Dispose(disposing);
}
}
//Example of some validation rules on property values
partial class CPDemoUserProfile
{
partial void OnLastApprovedDateChanged()
{
DateTime? value = this._LastApprovedDate;
if (value > DateTime.MaxValue || value < DateTime.MinValue)
this._LastApprovedDate = default(DateTime?);
}
}
使用工作流基础的业务逻辑
在 IIS 中托管工作流运行时
在本节中,我将不尝试深入探讨工作流基础的实现细节以及如何设计和执行工作流。但是,我将提及由于托管环境是 Web 应用程序而产生的一些问题。这两个主要问题是
- 缓存工作流运行时
- 强制同步执行工作流
第一个问题可以使用 HttpContext
类的 Application
对象来解决,以全局缓存运行时。运行时可以通过应用程序级别事件:Application_Start
和 Application_End
来启动和关闭。这些事件当然会在 Global.asax 文件中公开。
至于第二个问题。我们必须稍微谈谈工作流运行时引擎中的线程管理。这项任务(指工作流执行)是调度程序服务的责任,它是工作流运行时的核心服务之一。默认服务 DefaultWorkflowSchedulerService
使用线程池 **异步** 执行工作流。当然,鉴于 Web 请求的无状态性,这在 ASP.NET 环境中存在问题。因此,为了确保 **同步** 执行,我们必须将 ManualWorkflowSchedulerService
加载到运行时中。此服务使用宿主应用程序的线程来执行工作流,从而绕过了异步线程问题。
通常,有一个实用程序类来处理启动和终止运行时以及执行工作流是一个好主意。我不会列出我自己的实现,以免文章中充斥着代码列表,除非读者另外要求。因此,文章的其余部分假定存在这样一个实用程序类,其结构类似于

接下来的两节将说明如何将我们自定义提供程序最重要的两个方法(即 GetPropertyValues
和 SetPropertyValues
)实现为工作流。其他自定义提供程序方法的实现没有详细说明,留给读者有能力自行改进。
GetPropertyValues 工作流

下面是该工作流的 C# 代码翻译
public sealed partial class GetProfilePropertiesWorkflow : SequentialWorkflowActivity
{
public GetProfilePropertiesWorkflow()
{
InitializeComponent();
}
public SettingsPropertyCollection SettingsCollection { get; set; }
public Guid ApplicationGuid { get; set; }
public string UserName { get; set; }
public CPDemoUserProfile UserProfile { get; set; }
public SettingsPropertyValueCollection ProfileSettings { get; set; }
private void InitializePropertyCollection_ExecuteCode(object sender, EventArgs e)
{
//Very important to populate the SettingsPropertyValueCollection with
//property names from the custom user profile
ProfileSettings = new SettingsPropertyValueCollection();
IEnumerable<SettingsProperty> _collection =
SettingsCollection.Cast<SettingsProperty>();
foreach (SettingsProperty sp in _collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(sp);
ProfileSettings.Add(value);
}
}
private void GetUserProfile_ExecuteCode(object sender, EventArgs e)
{
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
UserProfile = (from u in db.aspnet_Users
where u.LoweredUserName ==
UserName.ToLower() && u.ApplicationId == ApplicationGuid
join p in db.CPDemoUserProfiles on u.UserId equals p.UserId
select p).SingleOrDefault<CPDemoUserProfile>();
}
private void PopulatePropertyValues_ExecuteCode(object sender, EventArgs e)
{
if (null != UserProfile)
{
IEnumerable<SettingsProperty> _collection =
SettingsCollection.Cast<SettingsProperty>();
Type type = UserProfile.GetType();
foreach (SettingsProperty sp in _collection)
{
SettingsPropertyValue value = ProfileSettings[sp.Name];
if (null != value)
{
if (value.UsingDefaultValue)
value.PropertyValue = Convert.ChangeType(
value.Property.DefaultValue, value.Property.PropertyType);
PropertyInfo pi = type.GetProperty(sp.Name);
object pv = pi.GetValue(UserProfile, null);
if (null != pv && !(pv is DBNull))
value.PropertyValue = pv;
value.IsDirty = false;
value.Deserialized = true;
}
}
}
}
}
SetPropertyValues 工作流

下面是该工作流的 C# 代码翻译。比 GetPropertyValues
稍微复杂一些!
public sealed partial class SetProfilePropertiesWorkflow : SequentialWorkflowActivity
{
public SetProfilePropertiesWorkflow()
{
InitializeComponent();
}
public Guid ApplicationGuid { get; set; }
public string UserName { get; set; }
public bool isUserAuthenticated { get; set; }
public aspnet_User UserEntity { get; set; }
public SettingsPropertyValueCollection ProfileSettings { get; set; }
private void GetUser_ExecuteCode(object sender, EventArgs e)
{
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
UserEntity = (from u in db.aspnet_Users
where u.ApplicationId == ApplicationGuid &&
u.LoweredUserName == UserName.ToLower()
select u).SingleOrDefault<aspnet_User>();
}
private void UserFoundCondition(object sender, ConditionalEventArgs e)
{
e.Result = (UserEntity != null) ? true : false;
}
private static class EntityConverter<T>
{
public static void CopyValues(SettingsPropertyValueCollection source, T target)
{
IEnumerable<SettingsPropertyValue> _source =
source.Cast<SettingsPropertyValue>();
foreach (SettingsPropertyValue sv in _source)
{
PropertyInfo pi = target.GetType().GetProperty(sv.Name);
if (null != pi && sv.IsDirty)//set only properties that changed.
{
//incase value could not be deserialized properly
if (sv.Deserialized && null == sv.PropertyValue)
pi.SetValue(target, DBNull.Value, null);
else
pi.SetValue(target, sv.PropertyValue, null);
}
}
}
}
private void UpdateUserProfile_ExecuteCode(object sender, EventArgs e)
{
CPDemoUserProfile profile = UserEntity.CPDemoUserProfile;
bool isOrphanUser = false;
//incase we have an orphan user record with no profile.
//Might happen if we delete
//a profile without deleting the corresponding user record.
if (null == profile)
{
profile = new CPDemoUserProfile();
isOrphanUser = true;
}
//Note:
//Using reflection to populate values.
//This way we ensure reusability of this logic
//across other projects.
EntityConverter<CPDemoUserProfile>.CopyValues(ProfileSettings, profile);
//Update user table with latest activity date
UserEntity.LastActivityDate = DateTime.Now;
profile.LastProfileUpdateDate = DateTime.Now;
if (isOrphanUser)
UserEntity.CPDemoUserProfile = profile;
DataContextManager<ProfileDataContext>.GetInstance().SubmitChanges();
}
private void CreateNewUserProfile_ExecuteCode(object sender, EventArgs e)
{
Guid guid = Guid.NewGuid();
aspnet_User newUser = new aspnet_User();
newUser.IsAnonymous = !isUserAuthenticated;
newUser.UserId = guid;
newUser.UserName = UserName;
newUser.LoweredUserName = UserName.ToLower();
newUser.LastActivityDate = DateTime.Now;
newUser.ApplicationId = ApplicationGuid;
CPDemoUserProfile profile = new CPDemoUserProfile();
EntityConverter<CPDemoUserProfile>.CopyValues(ProfileSettings, profile);
profile.LastProfileUpdateDate = DateTime.Now;
newUser.CPDemoUserProfile = profile;
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
db.aspnet_Users.InsertOnSubmit(newUser);
db.SubmitChanges();
}
}
整合
现在,让我们看看所有这些跨技术努力的成果,最终的自定义配置文件提供程序实现。正如我之前提到的,只实现了最重要的方法,其余的留给您才华横溢的编程双手!
public class CPDemoUserProfileProvider : ProfileProvider
{
private string _appName = String.Empty;
private Guid _appGuid = Guid.Empty;
private string _providerName = String.Empty;
public override void Initialize(string name, NameValueCollection config)
{
_appGuid = new Guid(config["ApplicationGUID"]);
_appName = config["ApplicationName"];
_providerName = name;
base.Initialize(name, config);
}
public override string Name
{
get { return _providerName; }
}
public override string ApplicationName
{
get { return _appName; }
set { return; }
}
public override SettingsPropertyValueCollection GetPropertyValues
(SettingsContext context, SettingsPropertyCollection collection)
{
string userName = (string)context["UserName"];
Dictionary<string, object> properties = new Dictionary<string, object>();
properties.Add("SettingsCollection", collection);
properties.Add("UserName", userName);
properties.Add("ApplicationGuid", _appGuid);
properties.Add("ProfileSettings", null);
Core.WorkflowManager.ExecuteWorkflow(typeof(
Workflows.UserProfileWorkflows.GetProfilePropertiesWorkflow),
properties);
return properties["ProfileSettings"] as
SettingsPropertyValueCollection;
}
public override void SetPropertyValues(SettingsContext context,
SettingsPropertyValueCollection collection)
{
string userName = (string)context["UserName"];
bool userIsAuthenticated = (bool)context["IsAuthenticated"];
Dictionary<string, object> properties =
new Dictionary<string, object>();
properties.Add("ProfileSettings", collection);
properties.Add("UserName", userName);
properties.Add("isUserAuthenticated", userIsAuthenticated);
properties.Add("ApplicationGuid", _appGuid);
Core.WorkflowManager.ExecuteWorkflow(typeof(
Workflows.UserProfileWorkflows.SetProfilePropertiesWorkflow),
properties);
}
public override int DeleteInactiveProfiles(ProfileAuthenticationOption
authenticationOption, DateTime userInactiveSinceDate)
{
int ret = -1;
using (TransactionScope ts = new TransactionScope())
{
ProfileDataContext db =
DataContextManager<ProfileDataContext>.GetInstance();
IEnumerable<CPDemoUserProfile> profilestoDelete =
from p in db.CPDemoUserProfiles
where p.aspnet_User.LastActivityDate
<= userInactiveSinceDate
&& (authenticationOption ==
ProfileAuthenticationOption.All
|| (authenticationOption ==
ProfileAuthenticationOption.Anonymous &&
p.aspnet_User.IsAnonymous)
|| (authenticationOption ==
ProfileAuthenticationOption.Authenticated
&& !p.aspnet_User.IsAnonymous))
select p;
ret = profilestoDelete.Count();
db.CPDemoUserProfiles.DeleteAllOnSubmit(profilestoDelete);
db.SubmitChanges();
ts.Complete();
}
return ret;
}
/*
* Custom ProfileManger methods
*/
public int GetTotalNumberofProfiles()
{
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
IEnumerable<CPDemoUserProfile> profiles = (from p in db.CPDemoUserProfiles
select p).DefaultIfEmpty();
return profiles.Count();
}
}
结论
我希望这篇文章能给他人带来一些益处和启发。这篇文章源于我目前在开发一个实现 ASP.NET 配置文件提供程序的 Web 应用程序的工作。我认为以一种同时包含 LINQ 和工作流基础的方式构建自定义提供程序会很有趣。因此,我决定将此设计作为我第一次 CodeProject 贡献的主题。
可以理解的是,我不能复制我当前的产品代码并将其用于本文,因此这里提供的代码是一个可行的代码骨架,可以被采用和增强。它被精简到了最基本。就个人而言,在生产代码中,我使用已编译查询来获得更好的性能、异步页面任务和强大的异常处理机制。
祝您编码愉快!
推荐阅读
- Apress 出版的《Pro WF》,作者 Bruce Bukovics
- Apress 出版的《Pro LINQ Object Relational Mapping with C# 2008》,作者 Vijay P Mehta
- O'Reilly 出版的《Programming WCF Services》,作者 Juval Lowy
- O'Reilly 出版的《Beautiful Code》,编辑 Oram & Wilson
我还会定期查看这些博客
历史
- 2008 年 11 月 27 日 - 提交第一个版本
- 2008 年 12 月 7 日 - 添加了包含源代码的可运行示例,进行了少量编辑性修改,并更新了 原型化 ProfileProvider 类的代码列表