How can i resolve assemblies references outside of bin folder of ASP.NET web-development server? This can be useful to not have copies of same dll's.
Nothing is working: probing element at web.config don't work, i can't set up domain because it do application manager, and i can't subscribe on the resolve assembly event because it's too late - when i can subscribe initialization is over. So what can i do?
We can use PreApplicationStartMethodAttribute
and mark them some public static void method(in web-project assembly) with no arguments. This can be done at AssemblyInfo.cs class
For example:
[assembly: PreApplicationStartMethod(
typeof(Web.Initializer), "Initialize")]
That method will be called before compilation but after processing of the web.config. So we must explicitly tell to the compiler witch assembly it need to use during compilation. Also we need to subscribe here on Assembly Resolve event so we can manage assemblies resolving. Here is example:
public static class Initializer
{
public static void Initialize()
{
AppDomain.CurrentDomain.AssemblyResolve += LoadFromCommonBinFolder;
var referAsm = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
foreach (var assemblyName in referAsm)
{
try
{
var curAsm = Assembly.Load(assemblyName);
BuildManager.AddReferencedAssembly(curAsm);
LoadChildReferences(curAsm);
}
catch {}
}
}
private static void LoadChildReferences(Assembly curAsm)
{
foreach (var assemblyName in curAsm.GetReferencedAssemblies())
{
try
{
BuildManager.AddReferencedAssembly(Assembly.Load(assemblyName));
}
catch {}
}
}
private static Assembly LoadFromCommonBinFolder(object sender, ResolveEventArgs args)
{
string commonBinFolder = System.Configuration.ConfigurationManager.AppSettings["CommonBinFolderPath"];
if (String.IsNullOrEmpty(commonBinFolder))
{
throw new InvalidOperationException("​​CommonBinFolderPath in the app.config isn't seted.");
}
string assemblyName = new AssemblyName(args.Name).Name;
string assemblyPath = Path.Combine(commonBinFolder, assemblyName);
if (!File.Exists(assemblyPath + ".dll"))
{
if (!File.Exists(assemblyPath + ".exe"))
{
//searching for resources
var ci = CultureInfo.CurrentUICulture;
assemblyPath = Path.Combine(commonBinFolder, ci.Name, assemblyName + ".dll");
if (!File.Exists(assemblyPath))
{
assemblyPath = Path.Combine(commonBinFolder, ci.Parent, assemblyName + ".dll");
if (!File.Exists(assemblyPath))
{
return null;
}
}
}
}
return Assembly.LoadFrom(assemblyPath);
}
}
At this case "Web.Project.Assembly" still must be located in the bin folder. Others assemblies can shared from any folder.
Assemblies that are included under compilation Element in the web.config file must be also in the bin folder or at sub folder with probing element setted.
In same cases we must also add to this code adding references to child assemblies.
Why use 'BuildManager.AddReferencedAssembly'?
In the 'Application_Start' method to bind event the 'AssemblyResolve' and set the Inherits with assembly name in the aspx page,no 'BuildManager.AddReferencedAssembly'.
Related
We have a need to be able to drop new projects/dlls into our main project and have the main project pick them up and be able to use them. It was decided to use AutoFac to handle this need. This way the main project would not need a direct reference to any of the other projects/dlls we want to use. Here is the global.asax:
var builder = new ContainerBuilder();
var directoryName = HttpContext.Current.Server.MapPath("~/bin/");
var cfg = new ModularityLoader(new ModularityConfig(Path.Combine(directoryName, #"Modules")), new Log4NetLogger(typeof(ModularityConfig)));
cfg.RegisterModulesFromCatalog(builder);
builder.RegisterType<AssembliesResolver>().AsImplementedInterfaces();
builder.RegisterType<AppsSecurityFeatureResolver>().AsImplementedInterfaces();
builder.RegisterType<FeaturesAutofacAuthoriztationFilter>()
.AsWebApiAuthorizationFilterFor<ApiController>().InstancePerApiRequest();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterWebApiFilterProvider(GlobalConfiguration.Configuration);
builder.RegisterControllers(typeof(MvcApplication).Assembly);
Container = builder.Build();
We have post-build events on all projects that copy their dlls into the bin directory of our main project. All of the dlls are loaded in here with var cfg = new ModularityLoader.
See image:
builder.Build() calls the following class in any of the dlls in our project that implements Autofac.Module such as this one:
public class AutofacModuleConfig : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.RegisterApiControllers(typeof(AutofacModuleConfig).Assembly);
}
}
Here is where the trouble with SignalR comes in. I would like to put RouteTable.Routes.MapHubs() into AutofacModuleConfig, but SignalR 2.0.0+ no longer supports this. Instead, it wants you to create a Startup class like this:
[assembly: OwinStartup("SignalRConfig", typeof(KL.Apps.TestHarness.SignalR.Startup))]
namespace KL.Apps.TestHarness.SignalR
{
public static class Startup
{
public static void Configuration(IAppBuilder app)
{
app.MapSignalR("/signalr", new HubConfiguration());
}
}
}
Note: To get this to work I added the following to the web.config:
<add key="owin:appStartup" value="SignalRConfig" />
SignalR also requires Hub classes like this:
[HubName("BatchHub")]
public class BatchHub : Hub
{
public void RemoveBatchRow(Guid batchId)
{
Clients.All.RemoveBatchRow(batchId);
}
}
Having the Startup class and Hub classes in my main project works perfectly. However, because of the unique nature of AutoFac, none of my Hubs from my external dlls are getting loaded up. I was thinking to try to find a way to force AutofacModuleConfig.Load to accept RouteTable.Routes.MapHubs(), but adding this line breaks the code.
I was thinking to go back to an earlier version of SignalR so I can use RouteTable.Routes.MapHubs() but I would really like to use the most up to date version.
Any ideas?
Thanks in advance.
EDIT 1:
I found this: https://code.google.com/p/autofac/wiki/SignalRIntegration
It seems to be the answer the the problem. However, RegisterHubs does not exist off of the builder object even after installing the Autofac.SignalR NuGet package...
var builder = new ContainerBuilder();
builder.RegisterHubs(Assembly.GetExecutingAssembly());
EDIT 2:
So this is what my AutofacModuleConfig looks like now, but my hubs are still not showing up in javascript.
namespace OurName.IOC
{
public class AutofacModuleConfig : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
AutofacSignalRConfig.Load(builder);
builder.RegisterApiControllers(typeof(AutofacModuleConfig).Assembly);
}
}
}
namespace Autofac.Integration.SignalR
{
public static class AutofacSignalRConfig
{
public static void Load(ContainerBuilder builder)
{
builder.RegisterHubs(Assembly.GetExecutingAssembly());
}
}
}
Still not working though. In javascript I cannot see my hubs.
$(document).ready(function(){
var myHub = $.connection.BatchHub;
});
myHub is null.
EDIT 3:
Tried this last night. I removed AutofacSignalRConfig and this is how the main Global.asax looks now:
var builder = new ContainerBuilder();
var directoryName = HttpContext.Current.Server.MapPath("~/bin/");
var cfg = new ModularityLoader(new ModularityConfig(Path.Combine(directoryName, #"Modules")), new Log4NetLogger(typeof(ModularityConfig)));
cfg.RegisterModulesFromCatalog(builder);
builder.RegisterType<AssembliesResolver>().AsImplementedInterfaces();
builder.RegisterType<AppsSecurityFeatureResolver>().AsImplementedInterfaces();
builder.RegisterType<FeaturesAutofacAuthoriztationFilter>()
.AsWebApiAuthorizationFilterFor<ApiController>().InstancePerApiRequest();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterWebApiFilterProvider(GlobalConfiguration.Configuration);
builder.RegisterControllers(typeof(MvcApplication).Assembly);
foreach (var assembly in cfg.LoadedAssemblies)
{
var hubs = builder.RegisterHubs(assembly);
}
Container = builder.Build();
GlobalHost.DependencyResolver = new Autofac.Integration.SignalR.AutofacDependencyResolver(Container);
Notice how I tried to take the output of cfg (which is a collection of all assemblies found in the bin folder) and then foreach through them in order to execute builder.RegisterHubs on each one. Still not picking up my hubs from any of those assemblies though.
Does there need to be a Startup class in each of my imported dlls too? If so, how would I go about calling it?
EDIT 4:
Tried this too with no success:
foreach (var assembly in cfg.LoadedAssemblies)
{
//var hubs = builder.RegisterHubs(assembly);
var a = assembly.GetExportedTypes().Where(x => x.BaseType == typeof (Hub));
foreach (var x in a)
{
builder.RegisterType(x);
}
}
See image for debug info:
EDIT 5:
Jim Bolla thank you for the input. Here is my first try at your suggestion.
public class AssemblyLocator : IAssemblyLocator
{
public IList<Assembly> GetAssemblies()
{
var directoryName = HttpContext.Current.Server.MapPath("~/bin/");
var cfg = new ModularityLoader(new ModularityConfig(Path.Combine(directoryName, #"Modules")), new Log4NetLogger(typeof(ModularityConfig)));
return cfg.LoadedAssemblies;
}
}
Also added this just above Container = builder.Build:
builder.RegisterType<AssemblyLocator>().As<IAssemblyLocator>().SingleInstance();
AssemblyLocator.GetAssemblies() is now getting called.
... Testing ...
EDIT 6:
Okay. So now when I throw a breakpoint into javascript, I can see $.connection.BatchHub which is great! However, when trying to call the hub method RemoveBatchRow on the server-side I get the following error:
"Using a Hub instance not created by the HubPipeline is unsupported."
Here is the stack trace:
at
Microsoft.AspNet.SignalR.Hubs.NullClientProxy.TryInvokeMember(InvokeMemberBinder
binder, Object[] args, Object& result) at CallSite.Target(Closure ,
CallSite , Object , Guid ) at
System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite
site, T0 arg0, T1 arg1) at
KL.Apps.WebIndex.Hubs.BatchHub.RemoveBatchRow(Guid batchId) in
d:\Source\Apps Framework
Modules\WebIndex\Main\Source\KL.Apps.WebIndex\Hubs\BatchHub.cs:line 20
at
KL.Apps.WebIndex.API.Batch.BatchLockController.TryLockBatchAsync(String
batchId) in d:\Source\Apps Framework
Modules\WebIndex\Main\Source\KL.Apps.WebIndex\API\Batch\BatchLockController.cs:line
30 at lambda_method(Closure , Object , Object[] ) at
System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass13.b__c(Object
instance, Object[] methodParameters) at
System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object
instance, Object[] arguments) at
System.Web.Http.Controllers.ReflectedHttpActionDescriptor.<>c__DisplayClass5.b__4()
at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1
func, CancellationToken cancellationToken)
I am calling the hub method from one of my remote dlls like this:
new BatchHub().RemoveBatchRow(gId);
You can see the definition for BatchHub above.
Any other ideas?
I created two web solutions in ASP.NET 4.0, A and B. And I added a data entry web form (DataEntery.aspx) into Solution A, with third party user controls and form authentication.
I want to re-use the web page DataEntery.aspx into solution B. What is the best method for it?
You could create a third web application called Solution C that will hold user controls with shared functionality. Than add a reference to Solution C in Solution A and B.
Example: HOW TO: Share ASP.NET Pages and User Controls Between Applications by Using Visual Basic .NET
One of the way to do this is to add you DataEntry.aspx and code behind classes as Link to your projects. Also, you will need to add logic to copy DataEntry.aspx page from shared directory where it is located to directory it is added to project. You can do this by writing command lines command in PreBuild event of your projects or create CopyLinkedContentFiles.targets file with the following content:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<BuildDependsOn>
CopyLinkedContentFiles;
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
<Target Name="CopyLinkedContentFiles">
<!-- Remove any old copies of the files -->
<Delete Condition=" '%(Content.Link)' != '' AND Exists('$(WebProjectOutputDir)\%(Content.Link)') "
Files="$(WebProjectOutputDir)\%(Content.Link)" />
<!-- Copy linked content files recursively to the project folder -->
<Copy Condition=" '%(Content.Link)' != '' " SourceFiles="%(Content.Identity)"
DestinationFiles="$(WebProjectOutputDir)\%(Content.Link)" />
</Target>
</Project>
And then include this target to your project which will reference to page added as link. You can do by adding the following lines to your web applications csproj files:
<Import Project="$(MSBuildProjectDirectory)\[RELATIVE PATH TO FILE]\CopyLinkedContentFiles.targets" />
This line should be added for example after this line in your csproj files:
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
The best method is to create a dll that has your web pages.
You can reference this dll to get the code included in your project.
You can load resources embedded in a dll by using the VirtualFile class. Here is an example
public class SVirtualFile : VirtualFile
{
private string m_path;
public SVirtualFile(string virtualPath)
: base(virtualPath)
{
m_path = VirtualPathUtility.ToAppRelative(virtualPath);
}
public override System.IO.Stream Open()
{
var parts = m_path.Split('/');
var assemblyName = parts[1];
var resourceName = parts[2];
assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName);
var assembly = System.Reflection.Assembly.LoadFile(assemblyName + ".dll");
if (assembly != null)
{
return assembly.GetManifestResourceStream(resourceName);
}
return null;
}
}
You need to hook into the application life cycle and register a VirtualPathProvider so that asp.net knows you are serving virtual files.
public static class AppStart
{
public static void AppInitialize()
{
SVirtualPathProvider sampleProvider = new SVirtualPathProvider ();
HostingEnvironment.RegisterVirtualPathProvider(sampleProvider);
}
}
public class SVirtualPathProvider : VirtualPathProvider
{
public SVirtualPathProvider()
{
}
private bool IsEmbeddedResourcePath(string virtualPath)
{
var checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith("~/Succeed.Web/", StringComparison.InvariantCultureIgnoreCase);
}
public override bool FileExists(string virtualPath)
{
return IsEmbeddedResourcePath(virtualPath) || base.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsEmbeddedResourcePath(virtualPath))
{
return new SVirtualFile(virtualPath);
}
else
{
return base.GetFile(virtualPath);
}
}
public override CacheDependency GetCacheDependency( string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (IsEmbeddedResourcePath(virtualPath))
{
return null;
}
else
{
return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
}
Refer:
How to use virtual path providers to dynamically load and compile content from virtual paths
When using msdeploy to do a sync operation, one of the things that MSDeploy checks to determine if a file should be synced or not is the attributes on the file (readonly, archive, etc.). If the attributes are different between the two copies of the file, then the file will be synced. Is there a way to tell MSDeploy to ignore file attributes when determining if a file should be synced or not?
It is possible, however, not merely on the command line. You'd have to build a custom DeploymentRuleHandler, like this:
namespace CustomRuleHandlers
{
using Microsoft.Web.Deployment;
[DeploymentRuleHandler]
internal class IgnoreFileAttributesRuleHandler : DeploymentRuleHandler
{
public override int CompareAttribute(DeploymentSyncContext syncContext, DeploymentObject destinationObject, DeploymentObjectAttribute destinationAttribute, DeploymentObject sourceObject, DeploymentObjectAttribute sourceAttribute, int currentComparison)
{
if ((destinationObject.Name.Equals("filePath", StringComparison.Ordinal))
&& destinationAttribute.Name.Equals("attributes", StringComparison.Ordinal))
{
return 0;
}
return currentComparison;
}
public override string Description
{
get { return "Ignores file attributes when determining if a file should be synched or not."; }
}
public override string FriendlyName
{
get { return "IgnoreFileAttributes"; }
}
public override string Name
{
get { return "IgnoreFileAttributes"; }
}
public override bool EnabledByDefault
{
get { return false; }
}
}
}
Compile that into an assembly (targeting .Net 3.5 for WebDeploy v2!) and put the assembly into the "Extensibility" subfolder in the WebDeploy folder (normally, C:\Program Files\IIS\Microsoft Web Deploy V2\Extensibility).
Then, you can easily leverage your custom rule when running msdeploy from the command-line by adding this argument:
-enableRule:IgnoreFileAttributes
Of course, that assembly needs to be present on both, the source and the target machine, of a sync operation.
Unfortunately, there's no easier way of getting there!
I'm starting a web application that contains the following projects:
Booking.Web
Booking.Services
Booking.DataObjects
Booking.Data
I'm using the repository pattern in my data project only. All services will be the same, no matter what happens. However, if a customer wants to use Access, it will use a different data repository than if the customer wants to use SQL Server.
I have StructureMap, and want to be able to do the following:
Web project is unaffected. It's a web forms application that will only know about the services project and the dataobjects project.
When a service is called, it will use StructureMap (by looking up the bootstrapper.cs file) to see which data repository to use.
An example of a services class is the error logging class:
public class ErrorLog : IErrorLog
{
ILogging logger;
public ErrorLog()
{
}
public ErrorLog(ILogging logger)
{
this.logger = logger;
}
public void AddToLog(string errorMessage)
{
try
{
AddToDatabaseLog(errorMessage);
}
catch (Exception ex)
{
AddToFileLog(ex.Message);
}
finally
{
AddToFileLog(errorMessage);
}
}
private void AddToDatabaseLog(string errorMessage)
{
ErrorObject error =
new ErrorObject
{
ErrorDateTime = DateTime.Now,
ErrorMessage = errorMessage
};
logger.Insert(error);
}
private void AddToFileLog(string errorMessage)
{
// TODO: Take this value from the web.config instead of hard coding it
TextWriter writer = new StreamWriter(#"E:\Work\Booking\Booking\Booking.Web\Logs\ErrorLog.txt", true);
writer.WriteLine(DateTime.Now.ToString() + " ---------- " + errorMessage);
writer.Close();
}
}
I want to be able to call this service from my web project, without defining which repository to use for the data access. My boostrapper.cs file in the services project is defined as:
public class Bootstrapper
{
public static void ConfigureStructureMap()
{
ObjectFactory.Initialize(x =>
{
x.AddRegistry(new ServiceRegistry());
}
);
}
public class ServiceRegistry : Registry
{
protected override void configure()
{
ForRequestedType<IErrorLog>().TheDefaultIsConcreteType<Booking.Services.Logging.ErrorLog>();
ForRequestedType<ILogging>().TheDefaultIsConcreteType<SqlServerLoggingProvider>();
}
}
}
What else do I need to get this to work? When I defined a test, the ILogger object was null.
Perhaps some details on how you are calling this code from a test would be useful.
My understanding is that you need to ensure that the ConfigureStructureMap call has been made early in the applications life (e.g. in the Global.asax in a web project).
After that you would be calling for instances of IErrorLog using something like:
IErrorLog log = StructureMap.ObjectFactory.GetNamedInstance<IErrorLog>();
The web applications I develop often require co-dependent configuration settings and there are also settings that have to change as we move between each of our environments.
All our settings are currently simple key-value pairs but it would be useful to create custom config sections so that it is obvious when two values need to change together or when the settings need to change for an environment.
What's the best way to create custom config sections and are there any special considerations to make when retrieving the values?
Using attributes, child config sections and constraints
There is also the possibility to use attributes which automatically takes care of the plumbing, as well as providing the ability to easily add constraints.
I here present an example from code I use myself in one of my sites. With a constraint I dictate the maximum amount of disk space any one user is allowed to use.
MailCenterConfiguration.cs:
namespace Ani {
public sealed class MailCenterConfiguration : ConfigurationSection
{
[ConfigurationProperty("userDiskSpace", IsRequired = true)]
[IntegerValidator(MinValue = 0, MaxValue = 1000000)]
public int UserDiskSpace
{
get { return (int)base["userDiskSpace"]; }
set { base["userDiskSpace"] = value; }
}
}
}
This is set up in web.config like so
<configSections>
<!-- Mailcenter configuration file -->
<section name="mailCenter" type="Ani.MailCenterConfiguration" requirePermission="false"/>
</configSections>
...
<mailCenter userDiskSpace="25000">
<mail
host="my.hostname.com"
port="366" />
</mailCenter>
Child elements
The child xml element mail is created in the same .cs file as the one above. Here I've added constraints on the port. If the port is assigned a value not in this range the runtime will complain when the config is loaded.
MailCenterConfiguration.cs:
public sealed class MailCenterConfiguration : ConfigurationSection
{
[ConfigurationProperty("mail", IsRequired=true)]
public MailElement Mail
{
get { return (MailElement)base["mail"]; }
set { base["mail"] = value; }
}
public class MailElement : ConfigurationElement
{
[ConfigurationProperty("host", IsRequired = true)]
public string Host
{
get { return (string)base["host"]; }
set { base["host"] = value; }
}
[ConfigurationProperty("port", IsRequired = true)]
[IntegerValidator(MinValue = 0, MaxValue = 65535)]
public int Port
{
get { return (int)base["port"]; }
set { base["port"] = value; }
}
Use
To then use it practically in code, all you have to do is instantiate the MailCenterConfigurationObject, this will automatically read the relevant sections from web.config.
MailCenterConfiguration.cs
private static MailCenterConfiguration instance = null;
public static MailCenterConfiguration Instance
{
get
{
if (instance == null)
{
instance = (MailCenterConfiguration)WebConfigurationManager.GetSection("mailCenter");
}
return instance;
}
}
AnotherFile.cs
public void SendMail()
{
MailCenterConfiguration conf = MailCenterConfiguration.Instance;
SmtpClient smtpClient = new SmtpClient(conf.Mail.Host, conf.Mail.Port);
}
Check for validity
I previously mentioned that the runtime will complain when the configuration is loaded and some data does not comply to the rules you have set up (e.g. in MailCenterConfiguration.cs). I tend to want to know these things as soon as possible when my site fires up. One way to solve this is load the configuration in _Global.asax.cx.Application_Start_ , if the configuration is invalid you will be notified of this with the means of an exception. Your site won't start and instead you will be presented detailed exception information in the Yellow screen of death.
Global.asax.cs
protected void Application_ Start(object sender, EventArgs e)
{
MailCenterConfiguration.Instance;
}
Quick'n Dirty:
First create your ConfigurationSection and ConfigurationElement classes:
public class MyStuffSection : ConfigurationSection
{
ConfigurationProperty _MyStuffElement;
public MyStuffSection()
{
_MyStuffElement = new ConfigurationProperty("MyStuff", typeof(MyStuffElement), null);
this.Properties.Add(_MyStuffElement);
}
public MyStuffElement MyStuff
{
get
{
return this[_MyStuffElement] as MyStuffElement;
}
}
}
public class MyStuffElement : ConfigurationElement
{
ConfigurationProperty _SomeStuff;
public MyStuffElement()
{
_SomeStuff = new ConfigurationProperty("SomeStuff", typeof(string), "<UNDEFINED>");
this.Properties.Add(_SomeStuff);
}
public string SomeStuff
{
get
{
return (String)this[_SomeStuff];
}
}
}
Then let the framework know how to handle your configuration classes in web.config:
<configuration>
<configSections>
<section name="MyStuffSection" type="MyWeb.Configuration.MyStuffSection" />
</configSections>
...
And actually add your own section below:
<MyStuffSection>
<MyStuff SomeStuff="Hey There!" />
</MyStuffSection>
Then you can use it in your code thus:
MyWeb.Configuration.MyStuffSection configSection = ConfigurationManager.GetSection("MyStuffSection") as MyWeb.Configuration.MyStuffSection;
if (configSection != null && configSection.MyStuff != null)
{
Response.Write(configSection.MyStuff.SomeStuff);
}
The custom configuration are quite handy thing and often applications end up with a demand for an extendable solution.
For .NET 1.1 please refer the article https://web.archive.org/web/20211027113329/http://aspnet.4guysfromrolla.com/articles/020707-1.aspx
Note: The above solution works for .NET 2.0 as well.
For .NET 2.0 specific solution, please refer the article https://web.archive.org/web/20210802144254/https://aspnet.4guysfromrolla.com/articles/032807-1.aspx
You can accomplish this with Section Handlers. There is a basic overview of how to write one at http://www.codeproject.com/KB/aspnet/ConfigSections.aspx however it refers to app.config which would be pretty much the same as writing one for use in web.config. This will allow you to essentially have your own XML tree in the config file and do some more advanced configuration.
The most simple method, which I found, is using appSettings section.
Add to Web.config the following:
<appSettings>
<add key="MyProp" value="MyVal"/>
</appSettings>
Access from your code
NameValueCollection appSettings = ConfigurationManager.AppSettings;
string myPropVal = appSettings["MyProp"];