Psake Builds and TeamCity Integration

Psake is a powerful build system, build on Powershell for a lot of functionality and familiarity. And TeamCity is a really nice continuous integration server, checkout the CodeBetter TeamCity install for a good demo. Hooking the two up was not a easy as using just Msbuild or Nant, but it’s not really too bad.

First, your Psake script needs to check the exit codes of the applications it calls. This is important to do regardless because Powershell doesn’t error out based on the exit code. The trunk version of Psake has a convenience function to execute a command and check it’s exit code is greater than 0, called exec:

You can also pass a message as the second argument.

With this checking in place, next is a bat script to use from the TeamCity command line build option.

@echo off
powershell -NoProfile -ExecutionPolicy unrestricted -Command "& {Import-Module ‘.\tools\psake\psake.psm1’; invoke-psake -t %1; if ($Error -ne ‘’) {write-host "ERROR: $error" -fore RED; exit $error.Count} }"`

This executes a Powershell prompt which imports the Psake module and then calls the provided Psake task. Finally, it checks the $Error variable and sets the exit code to the number of errors returned by the script. This will fail the TeamCity build and allows any error thrown by Powershell to break the build.

Last is to have TeamCity run the bat file with the desired target.

psake.bat Compile

One thing to note is that some things return exit code other then 0 but that are not errors. In that case you can check the Powershell variable $LastExitCode and throw errors based on that.

Thanks to the psake users group for all their help!


error CS1668: Warning as Error: Invalid search path ‘C:\Program Files\Microsoft SDKs\Windows\v6.0A\lib’ specified in ‘LIB environment variable’

I’m not sure what caused my setup to produce this error, but after searching for a while and not finding any reason, I just added the lib directory and all was good. I was getting this when doing command line compiles and when using the spark view engine. I hope this might save me or someone else some time in the future.

error CS1668: Warning as Error: Invalid search path ‘C:\Program Files\Microsoft SDKs\Windows\v6.0A\lib’ specified in ‘LIB environment variable’

A New Year & A New Venture: Red27 Consulting

business-card-frontI’ve decided to take the plunge and start consulting full time. My company, Red27 Consulting, has been around for few years, but this year I will be pursuing consulting/independent development work full time. Red27 will be an agile/alt.net/craftsmanship based business, building websites and web applications. We will partner with customers to build what they need quickly and with high quality. The challenge is how to show the value in these agile/alt.net/craftsmanship processes and principles to people not in software development. I believe, over time, using these processes and principles will prove to be a better way to build software. I will write about these experiences here along with the technical content that I’ve been writing.

Lastly, keep Red27 Consulting in mind when someone has a website or web application they want built!


Making Sure A NHibernate IInterceptor Is In The Session

Building on the previous post Intercepting NHibernate to Handle Additional Database Work, I used the ILifecycle interface to ensure the IInterceptor was setup on the session before saving MyEntity. I ILifecycle interface is deprecated, but I needed some way to make sure the interceptor was there. If an entity has the ILifecycle interface nothing more is needed, NHibernate will call the methods. I used the OnSave method, to inspect the session instance and throw and exception if needed. Anyway, here’s the code:

public class MyEntity : ILifecycle
{
    public virtual int Id { get; set; }
    public virtual int ParentId { get; set; }
  
    public virtual LifecycleVeto OnSave(ISession s)
    {
        var sessionDelegate = s as SessionDelegate;
        var session = sessionDelegate != null ? sessionDelegate.InnerSession as SessionImpl : s as SessionImpl;
  
        if (session != null && session.Interceptor.GetType().Equals(typeof(MyEntityInterceptor)))
            return LifecycleVeto.NoVeto;
  
        throw new ApplicationException("MyEntityInterceptor needs to be registered with the container.");
    }
  
    public virtual LifecycleVeto OnUpdate(ISession s)
    {
        return LifecycleVeto.NoVeto;
    }
  
    public virtual LifecycleVeto OnDelete(ISession s)
    {
        return LifecycleVeto.NoVeto;
    }
  
    public virtual void OnLoad(ISession s, object id)
    {
    }
}

I think there are potentially better ways to handle this, but with the existing constraints on the code and time work something out, this was the way I went.


Intercepting NHibernate to Handle Additional Database Work

Today I needed to take addition database action when an entity is saved or deleted. A trigger might have been an option, but I try to limit the use of triggers. NHibernate to the rescue! Since the code was already using NHibernate it was an easy choice. There are a couple of options here. First is to use a stored procedure for the insert. However, that requires not using identity columns and wouldn’t work in this case. There is the ILifecycle interface, but it is deprecated and doesn’t have after-save hooks. Which leaves the IInterceptor interface.

Sub-classing EmptyInterceptor, I overrode the needed methods. If the entity is the one I’m concerned with, I store it or it’s data for later use in a queue collection. In the PostFlush method the entities stored in the queue will have their Identity set and can be used to set the properties for the additional work. Also, the deleted object will have been deleted which is important. I’m not sure I needed lock the queue objects, but I guess there is a change the session could be used on multiple threads and to be save they are locked.

The named queries are stored in the MyEntity Mapping file using the sql-query element.

public class MyEntityInterceptor : EmptyInterceptor
{
    private ISession session; 
    private readonly Queue<MyEntity> saves = new Queue<MyEntity>();
    private readonly Queue<int> deletes = new Queue<int>(); 
  
    public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, global::NHibernate.Type.IType[] types)
    {
        var myEntity = entity as MyEntity;
  
        if (myEntity != null)
            saves.Enqueue(myEntity);
  
        return true;
    }
  
    public override void OnDelete(object entity, object id, object[] state, string[] propertyNames, global::NHibernate.Type.IType[] types)
    {
        var myEntity = entity as MyEntity;
  
        if (myEntity != null)
            lock (deletes) 
                if (!deletes.Contains(myEntity.ParentId))
                    deletes.Enqueue(myEntity.ParentId); 
    }
      
    public override void PostFlush(ICollection entities)
    {
        ExecuteSaves();
        ExecuteDeletes();
    }
      
    private void ExecuteSaves()
    {
        lock (saves) 
            while (saves.Count > 0) 
                {
                    var myEntity = saves.Dequeue();

                    session.GetNamedQuery("SqlToExecuteForSaves") 
                        .SetInt32("Id", myEntity.Id)
                        .ExecuteUpdate();
                }
    }
          
    private void ExecuteDeletes()
    {
        lock (deletes)
            while (deletes.Count > 0) 
                {
                    var id = deletes.Dequeue();

                    session.GetNamedQuery("SqlToExecuteForDeletes") 
                        .SetInt32("Id", id)
                        .ExecuteUpdate();
                }
        }
  
    public override void SetSession(ISession session)
    {
        session = session;
    }
}
              
public class MyEntity
{
    public int ParentId;
    public int Id;
}

Since it’s using the ISession and IQuery interface, unit testing was easy.

The code is also using the NHibernateFacility from the CastleProject. Which makes adding interceptors as easy as adding them to the container. The naming is important as that’s how the facility looks up the interceptors. The first will be used for all session factories unless there is on for the specific factory as the second option shows.

  container.Register(Component.For<IInterceptor>()
      .ImplementedBy<MyEntityInterceptor>()
      .Named("nhibernate.session.interceptor"));

  container.Register(Component.For<IInterceptor>()
      .ImplementedBy<MyEntityInterceptor>()
      .Named("nhibernate.session.interceptor.MyOtherFactoryAlias"));

N2CMS Build From Source

image

  1. Download source
  2. Run Prepare_Dependencies-vs2008.bat
  3. Deploy_Everything-vs2008.bat
  4. Copy the output/Templates_Mvc directory to a new directory for the projects
  5. Edit permissions on the wwwroot sub folder to add IIS_USERS if needed.
  6. x64 only: download SQLite and copy the x64 version of System.Data.SQLite.dll to wwwroot\bin
  7. Follow the instructions in the install.txt

Automating Releases with SFTP

I’m trying to automate the deployment process for a website. I’m using Psake for the build process and Migrator.Net for the db migrations. Which left the problem of getting the code to the server. I’ve taken a stab that this using this guide to setup SFTP with a restricted user with just SFTP access using public/private keys.

Next was to automate the file upload, first I just used at batch file and psftp.exe, this worked except it re-uploads everything, much of which didn’t change. So, next was FileZilla, no luck there. Finally, WinSCP which works ok, but it doesn’t seem to allow a fully automated sync of files, but it does get close.


Asp.Net MVC CheckBoxList HtmlHelper Extension

I added some more methods to the code provided by Tyler Garlick’s CheckBoxList for ASP.net MVC to allow a dictionary to be passed in place of a list of SelectListItems and an optional list of selected ids. Anyway, here’s the full class.

using System.Collections.Generic;
using System.Linq;
using System.Text;
  
namespace System.Web.Mvc
{
    public static class CheckBoxListHelper
    {
        public static string CheckBoxList(this HtmlHelper helper, string name, IDictionary<string, string> items)
        {
            return CheckBoxList(helper, name, items, null, null);
        }
  
        public static string CheckBoxList(this HtmlHelper helper, string name, IDictionary<string, string> items, IDictionary<string, object> checkboxHtmlAttributes)
        {
            return CheckBoxList(helper, name, items, null, checkboxHtmlAttributes);
        }
  
        public static string CheckBoxList(this HtmlHelper helper, string name, IDictionary<string, string> items, IEnumerable<string> selectedValues)
        {
            return CheckBoxList(helper, name, items, selectedValues, null);
        }
  
        public static string CheckBoxList(this HtmlHelper helper, string name, IDictionary<string, string> items, IEnumerable<string> selectedValues, IDictionary<string, object> checkboxHtmlAttributes)
        {
            var selectListItems = from i in items
                                  select new SelectListItem
                                             {
                                                 Text = i.Key,
                                                 Value = i.Value,
                                                 Selected = (selectedValues != null && selectedValues.Contains(i.Value))
                                             };
  
            return CheckBoxList(helper, name, selectListItems, checkboxHtmlAttributes);
        }
  
        public static string CheckBoxList(this HtmlHelper helper, string name, IEnumerable<SelectListItem> items)
        {
            return CheckBoxList(helper, name, items, null);
        }
  
        public static string CheckBoxList(this HtmlHelper helper, string name, IEnumerable<SelectListItem> items, IDictionary<string, object> checkboxHtmlAttributes)
        {
            var output = new StringBuilder();
  
            foreach (var item in items)
            {
                output.Append(&ldquo;&ldquo;);
                var checkboxList = new TagBuilder("input&rdquo;);
                checkboxList.MergeAttribute(&ldquo;type&rdquo;, &ldquo;checkbox&rdquo;);
                checkboxList.MergeAttribute(&ldquo;name&rdquo;, name);
                checkboxList.MergeAttribute(&ldquo;value&rdquo;, item.Value);
  
                // Check to see if it&rsquo;s checked
                if (item.Selected)
                    checkboxList.MergeAttribute(&ldquo;checked&rdquo;, &ldquo;checked&rdquo;);
  
                // Add any attributes
                if (checkboxHtmlAttributes != null)
                    checkboxList.MergeAttributes(checkboxHtmlAttributes);
  
                checkboxList.SetInnerText(item.Text);
                output.Append(checkboxList.ToString(TagRenderMode.SelfClosing));
                output.Append(&ldquo;  &rdquo; + item.Text + &ldquo;&rdquo;);
            }
  
            return output.ToString();
        }
    }
}

On the view you can pass a dictionary made from items in your view model and selected values form your view model.

<div>
    ${Html.CheckBoxList(&ldquo;Product.Categories&rdquo;,
      ViewData.Model.Categories.ToDictionary(c => c.Name, c => c.Id.ToString()),
      ViewData.Model.Product.Categories.Select(c => c.Id.ToString()))}
</div>

In the controller you can access the form values in the request as a comma list of values by the name of the form field.

Request.Form["Product.Categories"]

Hope this helps and thanks to Tyler Garlick for meat of the code.

[UPDATE] Tyler has updated is code for MVC 2.0, here.


NHibernate String Enumeration Mapping

Here’s what’s needed to have NHibernate map an enumeration by the enumeration name instead of the number.

<property name="DayOfWeek" type="NHibernate.Type.EnumStringType`1[System.DayOfWeek, mscorlib], NHibernate"></property>

NHibernate Session Per Request Using Castles WcfFacility

I was under the false impression that the NHibernateFacility would handle session by request, needed for lazy loading collections, when used with the WcfFacility in an IIS setting. This might work if you use ASP.NET compatibility mode, however, it’s not a recommended solution and I needed to support multiple database connections. Soooo … after searching and reading some other great posts to get me in the right direction I came up with the following solutions.

First the posts that helped me with this:

Second what I’m building on:

  • Castle.Windsor
  • Castle.NHibernateFacility
  • Castle.WcfFacility
  • Running on IIS

The solution:

First off we have a class used to hold the ISession references for the duration of the request.

public sealed class NHibernateContextExtension : IExtension, IDisposable
{
    public List Sessions { get; set; }
  
    public NHibernateContextExtension()
    {
        Sessions = new List();
    }
  
    public void Attach(InstanceContext owner)
    { }
  
    public void Detach(InstanceContext owner)
    { }
  
    public void Dispose()
    {
        if (Sessions != null)
            Sessions.ForEach(s => s.Close());
    }
}

Next we need to hook into the WCF call at the before and after invoke. This is where we create the needed ISession instances and store in the above class.

public class WcfSessionPerRequestCallContextInitializer : ICallContextInitializer
{
    private readonly ISessionManager sessionManager; 
    private readonly string[] dbAliases;
  
    public WcfSessionPerRequestCallContextInitializer(ISessionManager sessionManager, string[] dbAliases)
    {
        sessionManager = sessionManager; 
        dbAliases = dbAliases;
    }
  
    public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
    {
        var extension = new NHibernateContextExtension();
  
        foreach (var dbAlias in dbAliases) 
            extension.Sessions.Add(sessionManager.OpenSession(dbAlias));

        instanceContext.Extensions.Add(extension);
        return extension;
    }
    
    public void AfterInvoke(object correlationState)
    {
        if (correlationState != null)
            ((IDisposable)correlationState).Dispose();
    }
}

Now we needed to hook the ICallContextInitializer into WCF using a IServiceBehavior. The WcfFacility will add this to all WCF services if it’s registered on the container.

public class WcfSessionPerRequestBehavior : IServiceBehavior
{
    private readonly ISessionManager sessionManager; 
    private string[] dbAliases = { "nh.facility.default" };
    public string[] DbAliases
    {
        get { return dbAliases; } 
        set { dbAliases = value; }
    }

    public WcfSessionPerRequestBehavior(ISessionManager sessionManager)
    {
        sessionManager = sessionManager; 
    }

    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
    }
  
    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection endpoints, BindingParameterCollection bindingParameters)
    {
    }
  
    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
        foreach (var cdb in serviceHostBase.ChannelDispatchers)
        {
            var channelDispatcher = cdb as ChannelDispatcher;
            if (null != channelDispatcher)
            {
                foreach (var endpointDispatcher in channelDispatcher.Endpoints)
                {
                    foreach (var dispatchOperation in endpointDispatcher.DispatchRuntime.Operations)
                    {
                        dispatchOperation.CallContextInitializers.Add(new WcfSessionPerRequestCallContextInitializer(sessionManager, _dbAliases));
                    }
                }
            }
        }
    }
}

Lastly, register the behavior with the container and provide any aliases needed.

container.Register(
    Component.For()
        .ImplementedBy()
        .DependsOn(new Hashtable { { "DbAliases", new[] { "nh.facility.default" } } })
    );

Unit Testing WCF Security with Castles WcfFacility

Building off David Tchepak’s post “Faking WCF Authentication”, here’s some code to override the Service Host creation when using the Castle WcfFacility.

The main integration points are a custom IWcfServiceModel and IServiceHostBuilder using the custom model. Both points have abstract base classes to build from. The model in this case is just an empty class used by the facility for finding the builder.

public class TestSecurityServiceModel : WcfServiceModel<TestSecurityServiceModel>
{
}

The service host builder is where we inject the authorization data we want available in our services. This host and authorization code are from David’s post, check out that for the TestAuthPolicy, TestPrincipal, and TestIdentity code. Here’s the builder.

public class SecurityDefaultServiceHostBuilder : AbstractServiceHostBuilder<TestSecurityServiceModel>
{
    public SecurityDefaultServiceHostBuilder(IKernel kernel)
        : base(kernel)
    {
    }
  
    protected override ServiceHost CreateServiceHost(ComponentModel model, TestSecurityServiceModel serviceModel, params Uri[] baseAddresses)
    {
        return CreateServiceHost(model, GetEffectiveBaseAddresses(serviceModel, baseAddresses));
    }
  
    protected override ServiceHost CreateServiceHost(ComponentModel model, Uri[] baseAddresses)
    {
        return InjectAuthorizationPolicies(new DefaultServiceHost(model, baseAddresses));
    }
  
    protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
        return InjectAuthorizationPolicies(new DefaultServiceHost(serviceType, baseAddresses));
    }
  
    private static ServiceHost InjectAuthorizationPolicies(ServiceHost host)
    {
        host.Authorization.ExternalAuthorizationPolicies
            = new List<IAuthorizationPolicy> { new TestAuthPolicy() }.AsReadOnly();
  
        host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.Custom;
  
        return host;
    }
}

It’s creating an instance of the DefaultServiceHost and added the authorization properties needed before returning the created service host.

Lastly, we need to integrate this with the WcfFacility. This is don’t at two points, first we need to tell the facility about our custom building and model with the AddServiceHostBuilder method.

var facility = new WcfFacility();
_container.AddFacility("wcf", facility);
facility.Services.AddServiceHostBuilder<SecurityDefaultServiceHostBuilder, TestSecurityServiceModel>();

Then when registering a service we set the ActAs model to the new TestSecurityServiceModel. This tells the facility to use the SecurityDefaultServiceHostBuilder to the service host.

Component.For<IService>()
    .ImplementedBy<Service>()
    .Named("MyService")
    .ActAs(new TestSecurityServiceModel()
    .AddEndpoints(
        WcfEndpoint.BoundTo(new NetTcpBinding { PortSharingEnabled = true })
            .At(MyServiceAddress))),

Now, when a client channel connects to this service the service host will be built by the new builder and have the authorization data needed to test the service.

IService client = ChannelFactory<IService>.CreateChannel(
    new NetTcpBinding { PortSharingEnabled = true }, new EndpointAddress(MyServiceAddress));
client.ServiceCall(1);

This was used for unit/integration tests, but could be used to simply extend the service host when used in the WcfFacility.


Getting Started With The Spark View Engine

Here’s some quick notes for getting started with the Spark View Engine on ASP.NET MVC, including setting up a container (Castle Windsor).

Spark

  1. Reference the Spark assemblies Spark.dll, Spark.Web.Mvc.dll, and SparkLanguage.

  2. Configure and add the view engine in the MvcApplication class (full class below)

    var settings = new SparkSettings() .SetDebug(true) //.SetPageBaseType(“Your.NonDefault.BaseSparkView”) //.AddAssembly(“YourAssembly”) .AddNamespace(“System”) .AddNamespace(“System.Collections.Generic”) .AddNamespace(“System.Linq”) .AddNamespace(“System.Web.Mvc”) .AddNamespace(“System.Web.Mvc.Html”);

    ViewEngines.Engines.Add(new SparkViewFactory(settings));

  3. Add an Application.spark file to the Shared Views folder. The engine looks for this file as the site master layout file. It will also look for one with the controller name to use for views related to that controller, ex, Home.spark for the HomeController.

    <use content="title" />

Apart from the general syntax, there are two things to note both regarding the Use tag. File looks for other views in the shared folder to render. While the content looks for content to render in it’s place which can be set in a sub view, as below. content=”view” is the view being rendered.

  1. The view, Index.spark, is just a normal view. The tag defines the content used for the title in the Application.spark view above.

    My Home Page </assets:title>

    ${"Hello world"}

    ${Guid.NewGuid().ToString("n")}

The documentation on the Spark site is pretty good.

Container Setup

Thanks to Matt Halls post for pointing me to the integration point for adding a container to MVC for controller creation, the IControllerFactory.

public class ControllerFactory : IControllerFactory
{
    private readonly IWindsorContainer container; 
    public ControllerFactory(IWindsorContainer container)
    {
        container = container;
    }

    public IController CreateController(RequestContext requestContext, string controllerName)
    {
        return (IController) container.Resolve(controllerName.ToLowerInvariant() + "controller"); 
    }

    public void ReleaseController(IController controller)
    {
        container.Release(controller);
    }
}

Next setup the container in the MvcApplication class and set the factory in MVC with the ControllerBuilder.Current.SetControllerFactory method. Also register the controllers in the container.

public class MvcApplication : HttpApplication
{
    private WindsorContainer container; 
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
  
        routes.MapRoute(
            "Default",                                              // Route name
            "{controller}/{action}/{id}",                           // URL with parameters
            new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
        );
    }
  
    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);
  
        ControllerBuilder.Current.SetControllerFactory(GetControllerFactory());
  
        var settings = new SparkSettings()
            .SetDebug(true)
            //.SetPageBaseType("Your.NonDefault.BaseSparkView")
            //.AddAssembly("YourAssembly")
            .AddNamespace("System")
            .AddNamespace("System.Collections.Generic")
            .AddNamespace("System.Linq")
            .AddNamespace("System.Web.Mvc")
            .AddNamespace("System.Web.Mvc.Html");
  
        ViewEngines.Engines.Add(new SparkViewFactory(settings));
    }
  
    private IControllerFactory GetControllerFactory()
    {
        container = new WindsorContainer();
  
        container.Register(AllTypes 
              .Of<IController>()
              .FromAssembly(typeof (HomeController).Assembly)
              .Configure(c => c.Named(c.ServiceType.Name.ToLowerInvariant()).LifeStyle.Transient));

        container.AddFacility("logging", new LoggingFacility(LoggerImplementation.Trace));
  
        return new ControllerFactory(_container);
    }
}

Changing The FluentNHibernate Id Conventions

FluentNhibernate provides a bunch of Convention interfaces and base classes to override the default conventions. In this case our database naming requires Id columns to include the table name like ProductId for the Products table. Also, we’re using the HiLo algorithm. This can be done with a simple class the implements IIdConvention.

public class FullIdNameConvention : IIdConvention
{
    public void Apply(IIdentityInstance instance)
    {
        instance.Column(instance.EntityType.Name + "Id");
        instance.GeneratedBy.HiLo("100");
    }
}

The convention also required referenced id to be named as Id where the default is _id. This can be changed with the follow two classes.

public class ManyToManyIdNameConvention : IHasManyToManyConvention
{
    public void Apply(IManyToManyCollectionInstance instance)
    {
        instance.Key.Column(instance.EntityType.Name + "Id");
        instance.Relationship.Column(instance.Relationship.StringIdentifierForModel + "Id");
    }
}
  
public class ReferenceIdNameConvention : IReferenceConvention
{
    public void Apply(IManyToOneInstance instance)
    {
        instance.Column(instance.Name + "Id");
    }
}

Some cases may require other interface implementations.

Lastly, FluentNhibernate needs to know about these conventions which can be on the AutoPersistenceModel class passed to the NHibernate configuration. This is done in an implementation of IConfigurationBuilder passed to the Castle NHibernate Facility.

var model = new AutoPersistenceModel()
    .AddEntityAssembly(typeof(Product).Assembly)
    .Where(t => t.Namespace.StartsWith(typeof(Product).Namespace))
    .Conventions.AddFromAssemblyOf<FullIdNameConvention>()
    .UseOverridesFromAssemblyOf<ProductMapping>()
    ;
  
configuration.AddAutoMappings(model);

More info can be found on the FluentNHibernate wiki.


NServiceBus 1.9 to 2.0 Upgrade Issues

Here’s some notes about breaking changes we ran into upgrading from 1.9 to 2.0 of NServiceBus.

  • The distributor. This was mostly due to the fact that we had our own TopShelf implementation and had to remove that stuff.
  • The auto loading of message handlers now throws on dll’s and exe’s that are not .net assemblies. I think this is better, but some of our projects reference native dll’s and so we changed our code to explicitly load the needed assemblies with the Configure.With(params Assemblies[] assemblies) overload.
  • Configuration Files. A lot of code was moved from NServiceBus.dll to NServiceBus.Core.dll and the config references need to be updated to reflect that. This was often resulting in “ComponentActivator: could not instantiate NServiceBus.Unicast.UnicastBus” errors. Mostly this was a problem for us regarding logging, but I think many of the other configuration section handlers moved to NServiceBus.Core too.
    • Common.Logging.ConfigurationSectionHandler, NServiceBus.Core
    • Common.Logging.Log4Net.Log4NetLoggerFactoryAdapter, NServiceBus.Core
  • Installing into our existing WindsorContainer. This was a fairly easy and welcome change, consisting of mostly deleting code and passing our container in the Configure.With().CastleWindsorBuilder(containerInstance) method.

Overriding NServiceBus Configuration

I needed to override the default NServiceBus configuration (app.config/web.config). This turns out to be really easy with the 2.0 NServiceBus release.

First, when setting up the Bus I needed to call CustomConfigurationSource(myCustomSource) like this:

var unicastBusConfig = configure.CastleWindsorBuilder(container) 
      .CustomConfigurationSource(myCustomSource)
      .XmlSerializer("http://tempuri.com")
      .MsmqTransport()
          .PurgeOnStartup(purgeOnStartup)
      .UnicastBus();

Second, I needed to implement IConfigurationSource on my myCustomSource class. This default implementation takes the passed in type and creates the configuration section handler of that type. To override a specific configuration section you can create an instance of that type, set it’s properties and return it. Otherwise, create and return the class without overriding any values.

There are four types of configuration sections used in NServiceBus currently, MsmqSubscriptionStorageConfig, DBSubscriptionStorageConfig, MsmqTransportConfig, and UnicastBusConfig.

public T GetConfiguration() where T : class
{
    if (typeof(T) == typeof(MsmqSubscriptionStorageConfig))
        return ConfigurationManager.GetSection(typeof(T).Name) as T;
  
    if (typeof(T) == typeof(DBSubscriptionStorageConfig))
        return ConfigurationManager.GetSection(typeof(T).Name) as T;
  
    if (typeof(T) == typeof(MsmqTransportConfig))
        return MsmqTransportConfiguration() as T;
  
    if (typeof(T) == typeof(UnicastBusConfig))
        return UnicastBusConfiguration() as T;
  
    return null;
}

Finally, the MsmqTransportConfiguration method creates a instance of MsmqTransportConfig and sets it’s values. In this case to fields that are passed in to the constructor, but it could be some other strategy or custom settings class.

private MsmqTransportConfig MsmqTransportConfiguration()
{
    return new MsmqTransportConfig
               {
                   ErrorQueue = errorQueue, 
                   InputQueue = inputQueue,
                   MaxRetries = maxRetries, 
                   NumberOfWorkerThreads = numberOfWorkerThreads
               };
}

Setting the Powershell Console’s Title to the Current SVN URL

I’m testing out branching per feature, as such I wanted a way to know where my working directory was pointing. Since I’m using power shell I added some functions to my profile script to set the window title to the svn url. This doesn’t auto update w/ an svn switch, but set-svntitle can be called manually.

Anyway, here’s the script.

$projects = "C:\data\working"
function set-title{Param([string] $title); $Host.Ui.RawUi.WindowTitle = $title}

function set-svntitle
{
  $currentsvn = svn info $projects | select-string &quot;URL: &quot;
  set-title $currentsvn 
}

set-svntitle 

NServiceBus Distributor Overview

Overview

The client sends messages to the distributors input queue on a remote machine. The server (or receiver) contacts the distributor asking for messages. As messages are sent to the distributor, it looks up the message endpoint in its configuration and sends the message to one of the servers waiting for work. It won’t send more to that server until it calls back to the distributor asking for more work. This stops those servers from ending up with a ton of unprocessed messages. Ayende has a good review of the Distributor here. The Colour Coding blog and this post on the Mailing list have more information about getting the samples working.

Client

The client configures the Message to go to the distributor data bus. This will be a remote queue in a distributed system.

  <MsmqTransportConfig
    InputQueue="client"
    ErrorQueue="error"
    NumberOfWorkerThreads="1"
    MaxRetries="5"
  />
  
  <UnicastBusConfig 
      DistributorControlAddress="" 
      DistributorDataAddress="">
    <MessageEndpointMappings>
      <add Messages="Messages" Endpoint="distributordatabus@xxxxxx0032" />
    </MessageEndpointMappings>
  </UnicastBusConfig>

Server

The server configures the Distributor control and data addresses on the UnicastBusConfig.

  <MsmqTransportConfig
    InputQueue="messagebus"
    ErrorQueue="error"
    NumberOfWorkerThreads="1"
    MaxRetries="5"
  />
  
  <UnicastBusConfig 
      DistributorControlAddress="distributorcontrolbus@xxxxxx0032" 
      DistributorDataAddress="distributordatabus@xxxxxx0032">
    <MessageEndpointMappings>
    </MessageEndpointMappings>
  </UnicastBusConfig>

Distributor

This distributor using the distributor control bus for it’s input. Thd distributor data bus in setup in the appSettings along with the distributor storage queue. The UnicastBusConfig need to have the message objects it will receive with the input queue of the server, messagebus in this case. This value should not be a remote value as the server will contact the distributor asking for work. It’s important for the DistributorControlAddress and DistributorDataAddress to be empty otherwise the Distributor will be in an endless loop sending stuff to itself. It would seem that the distributor would know which workers can handle which messages, but it seems like that is not the case. In other words, if you want the server to be a client in some cases with the client being the server in those cases, you need to setup another distributor. It helps to think of the Distributor as the Distributor for a specific service. Like the Business Process 1 Distributor.

  <MsmqTransportConfig
      InputQueue="distributorControlBus"
      ErrorQueue="error"
      NumberOfWorkerThreads="1"
      MaxRetries="5"
  />
  
  <UnicastBusConfig DistributorControlAddress="" DistributorDataAddress="">
    <MessageEndpointMappings>
      <add Messages="Messages" Endpoint="messagebus" />
    </MessageEndpointMappings>
  </UnicastBusConfig>
  
  <appSettings>
    <add key="DataInputQueue" value="distributorDataBus"/>
    <add key="NumberOfWorkerThreads" value="1"/>
    <add key="ErrorQueue" value="error"/>
    <add key="StorageQueue" value="distributorStorage"/>
    <add key="NameSpace" value="http://www.UdiDahan.com"/> <!&mdash; relevant for a Serialization of "interfaces" or "xml" &mdash;>
    <add key="Serialization" value="xml"/> <!&mdash; can be either "xml", or "binary" &mdash;>
  </appSettings>


N2cms Navigation Options on a MVC Site

Add the n2 SlidingCurtain control with a DragDropControlPanel control inside it after the opening body tag. This will link to the editing system of n2cms.

<n2:SlidingCurtain ID="SlidingCurtain1" runat="server">
    <n2:DragDropControlPanel ID="DragDropControlPanel1" runat="server" />
</n2:SlidingCurtain>

Now there are a bunch of N2 functions that can be used to interface with the navigation system.

To setup the menu the following can be used, the first list item shows a link to the home page. The second list item shows the sub pages.

<ul id="menu">
    <li><a href="<%= N2.Find.StartPage.Url %>">Home</a></li>
    <%= N2.Web.Tree.From(N2.Find.StartPage, 2).Filters(new N2.Collections.NavigationFilter()).ExcludeRoot(true)%>
</ul>

Next to setup the sub navigation something like this could be used.

<div class="leftColumn" style="border:solid 1px black">
    <h2><%= N2.Utility.Evaluate(N2.Find.AtLevel(N2.Find.CurrentPage, N2.Find.StartPage, 2), "Title") %></h2>
    <ul>
        <%= N2.Web.Tree.Between(N2.Find.CurrentPage, N2.Find.StartPage, true, 2).Filters(new N2.Collections.NavigationFilter()).ExcludeRoot(true) %>
    </ul>
</div>

Bread crumbs can be setup using.

<% foreach(N2.ContentItem item in N2.Find.EnumerateBetween(N2.Find.StartPage, N2.Find.CurrentPage, false)) { %>
    <%= N2.Web.Link.To(item) %> /
<% } %>
<%= N2.Utility.Evaluate(N2.Find.CurrentPage, "Title") %>

Lastly, you can update the page title using something similar to what’s been used above.

<title><%= N2.Utility.Evaluate(N2.Find.CurrentPage, "Title") %><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>

Setup N2Cms to Use the Asp.Net Providers for Authentication and Authorization

</p>

Continued from Setup N2Cms on an Asp.Net MVC

Setup the data

Add the asp.net sql tables by executing aspnet_regsql.exe

aspnet_regsql.exe -C "data source=.\SQLEXPRESS;Integrated Security=SSPI ;User Instance=true" -A all -d c:\projects\red27\app_data\red27.mdf

http://weblogs.asp.net/lhunt/archive/2005/09/26/425966.aspx

Next add the Administrators and Editors roles to the db using the aspnet_Roles_CreateRole stored procedure.

aspnet_Roles_CreateRole &lsquo;/&rsquo;, &lsquo;Administrators&rsquo; <br />aspnet_Roles_CreateRole &lsquo;/&rsquo;, &lsquo;Editors&rsquo;

Next update the web config roleManager to be enabled and remove all but the AspNetSqlRoleProvider provider.

http://code.google.com/p/n2cms/wiki/WebConfig

<roleManager enabled="true">
  <providers>
    <clear />
    <add connectionStringName="N2CMS" applicationName="/" name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
  </providers>
</roleManager>

Create the admin account

Now create a new user by going to the /Account/Register page. Then log off.

Log in to the /edit section of the site using the credentials found in the forms authentication section.

<authentication mode="Forms">
  <forms loginUrl="edit/login.aspx" protection="All" timeout="30000" path="/">
    <credentials passwordFormat="Clear">
      <user name="admin" password="changeme"/>
    </credentials>
  </forms>
</authentication>

Click on Manage Users and then edit your newly created account, adding it to the Administrators and Editors roles. Log off and back on with your new account.

Clean Up

Now remove the credentials section from the forms authentication section.

<authentication mode="Forms">
  <forms loginUrl="edit/login.aspx" protection="All" timeout="30000" path="/"/>
</authentication>

Open the web.config in the Edit directory and remove the users attribute from the authorization section.

<authorization>
  <allow roles="Administrators,Editors"/>
  <deny users="*"/>
</authorization>

Setup N2Cms on an Asp.Net MVC

The following are my notes on setting up a n2cms site starting with a default MVC project and copying in the needed stuff from the Example_Mvc sample from the n2cms site.

Starting with a new MVC site. Reference the dll’s in the \Example_Mvc\Mvc\wwwroot\Bin folder. I copied them to an externals folder under my solution and referenced them there.

Global.asax

  1. Add IEngine parameter to the RegisterRoutes method.

  2. Add a new ContentRoute to the RouteCollection.

  3. Add an ignore for .ashx routes.

  4. Initialize the N2 engine and pass to the RegisterRoutes in the Application_Start()

  5. Override Init and attach this to the EventRroker instance.

 

    public class MvcApplication : System.Web.HttpApplication

    {

        public static void RegisterRoutes(RouteCollection routes, IEngine engine)

        {

            routes.IgnoreRoute("{resource}.axd/{pathInfo}"</span>);</p>

            routes.IgnoreRoute("{resource}.ashx/{</em>pathInfo}");

 

            // This route detects content item paths and executes their controller

            routes.Add(new ContentRoute(engine));

 

            routes.MapRoute(

                "Default",                                              // Route name

                "{controller}/{action}/{id}",                           // URL with parameters

                new { controller = "Home", action = "Index", id = ""// Parameter defaults

            );

 

        }

 

        protected void Application_Start()

        {

            // normally the engine is initialized by the initializer module but it can also be initialized this programmatically

            // since we attach programmatically we need to associate the event broker with a http application

            IEngine engine = N2.Context.Initialize(false);

 

            RegisterRoutes(RouteTable.Routes, engine);

        }

 

        public override void Init()

        {

            EventBroker.Instance.Attach(this);

            base.Init();

        }

    }

</p></div>

Web.config

Add the n2 config sections.

 

    <sectionGroup name="n2" type="N2.Configuration.SectionGroup, N2">

      <section name="host" type="N2.Configuration.HostSection, N2" requirePermission="false"/>

      <section name="engine" type="N2.Configuration.EngineSection, N2" requirePermission="false"/>

      <section name="database" type="N2.Configuration.DatabaseSection, N2" requirePermission="false"/>

      <section name="edit" type="N2.Configuration.EditSection, N2" requirePermission="false"/>

    </sectionGroup>

</p>

Change the connection string name to N2CMS

Add the n2 section.

<n2>

    <!— If you install a database from scrach you’ll need to insert some required pages. This can be done by the web based installer located at http://yoursite/install/edit —>

    <host rootID="1" startPageID="1">

      <web extension="" rewrite="None">

        <urls enableCaching="false"/>

      </web>

    </host>

    <engine>

      <assemblies>

        <!— These are only needed for medium trust

       

       

        —>

      </assemblies>

    </engine>

    <!— Other flavours: SqlServer2005, SqlServer2000, MySql, SqLite —>

    <database connectionStringName="N2CMS" flavour="SqlServer2005"/>

    <edit>

      <installer checkInstallationStatus="true"/>

    </edit>

  </n2>

</p>

Add expressionBuilders to the system.web compilation section.

 

      <expressionBuilders>

        <add expressionPrefix="CurrentItem" type="N2.Web.Compilation.CurrentItemExpressionBuilder, N2"/>

        <add expressionPrefix="CurrentPage" type="N2.Web.Compilation.CurrentPageExpressionBuilder, N2"/>

        <add expressionPrefix="Code" type="N2.Web.Compilation.CodeExpressionBuilder, N2"/>

        <add expressionPrefix="StartPage" type="N2.Web.Compilation.StartPageExpressionBuilder, N2"/>

        <add expressionPrefix="HasValue" type="N2.Web.Compilation.HasValueExpressionBuilder, N2"/>

      </expressionBuilders>

</p>

Change the forms authentication section.

      <forms loginUrl="edit/login.aspx" protection="All" timeout="30000" path="/">

      </forms>

</p>

Add the n2 tag prefix to the pages controls section.

        <add tagPrefix="n2" assembly="N2" namespace="N2.Web.UI.WebControls"/>

</p>

Add the n2.ashx httpHandler

      <add path=".n2.ashx</span>" verb="</em>" type="N2.Web.AjaxRequestHandler, N2" /></p> </p></div>

Add a handler to the system.webServer handlers section.

      <add name="n2.ajax" path=".n2.ashx</span>" verb="</em>" type="N2.Web.AjaxRequestHandler, N2" /></p> </p></div>

The Start and Root Controller

Now we need to add a a ContentController class to the Controllers folder. And an AbstractPage and ContentPage to the Models folder. And a Content folder and DefaultView.aspx page the Views\Content folder. This will become the start and root nodes for the N2 CMS.

Models\AbstractPage.cs

using N2;

using N2.Details;

 

namespace Red27.Site.Models

{

    [WithEditableTitle, WithEditableName]

    public class AbstractPage : ContentItem, INode

    {

        public string PreviewUrl

        {

            get { return Url; }

        }

    }

}

</p>

Models\ContentPage.cs

using N2;

using N2.Details;

 

namespace Red27.Site.Models

{

    [Definition("Content Page", Installer = N2.Installation.InstallerHint.PreferredStartPage)]

    public class ContentPage : AbstractPage

    {

        [EditableFreeTextArea("Text", 100)]

        public virtual string Text

        {

            get { return (string)(GetDetail("Text") ?? string.Empty); }

            set { SetDetail("Text", value, string.Empty); }

        }

 

        public override string TemplateUrl

        {

            get { return "/Views/Content/DefaultView.aspx"; }

        }

    }

}

</p>

Controllers\ContentController.cs

using N2.Web;

using Red27.Site.Models;

using N2.Web.Mvc;

 

namespace Red27.Site.Controllers

{

    [Controls(typeof(AbstractPage))]

    public class ContentController : ContentController<AbstractPage>

    {

    }

}

</p>

Views\Content\DefaultView.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true"

    Inherits="ViewPage"</span> %> </p>

 

<%@ Import Namespace="Red27.Site.Models" %>

<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">

    <h1>

        <%= Model.Title %></h1>

    <%= Model.Text %>

</asp:Content>

</p> </div>

Initial DB Setup

Copy the Edit directory from Example_Mvc\Mvc\wwwroot to the root of your web project. This is the code that handles setup and content management and shouldn’t need to be modified. Add a new mdf database in the app_data directory or point the connection string to some other DB.

Now the site can be compiled and run. This should bring up the /edit/install page to create the db tables and insert the root node value. You may need to stop the development server and restart it to see your ContentPage in the root and start node drop downs. Once this is done you should be able to edit the content of your Start Page!

What’s Next?

A few more details need to be handled, like authentication, some N2 setup on the Site.Master page, and some clean up of unused pages from the MVC project setup.

</p>


Unit Testing NServiceBus Send Methods

Here’s a static method for helping test the Send calls when using NService Bus. It depends on RhinoMocks.
 

public static class BusExpectationExtensions

{

    public static IBus ExpectSend(this IBus bus, IMessage firstMessage, IMessage returnMessage)

    {

        var callback = MockRepository.GenerateStub<ICallback>();

 

        bus.Expect(b => b.Send(firstMessage)).IgnoreArguments().Return(callback);

 

        callback.Expect(c => c.Register(null, null)).IgnoreArguments()

            .Return(new BusAsyncResult(null, null))

            .WhenCalled(invocation =>

            {

                var ar = new BusAsyncResult(invocation.Arguments[0] as AsyncCallback, invocation.Arguments[1]);

                ar.Complete(0, returnMessage);

                invocation.ReturnValue = ar;

            });

        return bus;

    }

}

</p>

To use it, create a stub of IBus, and then call ExpectSend with your send and return messages like;

var bus = MockRepository.GenerateStub<IBus>()

    .ExpectSend(new SendMessage(2, 22), new MessageReturned(2, 22, 3.00m))

    .ExpectSend(new SendMessage(4, 44), new MessageReturned(4, 44, 234.34m));

 

var filter = new SomeFilteringClass(bus);

 

… execute and assert

</p>

Set Up Virtual Directories From The Build

Following up my previous post, an approach to extending msbuild, here’s what we ended up using to setup out web sites and services. We have a very service oriented architecture here and are starting to have a lot of web services that need to be setup before any testing or debugging can take place.

The build file takes some arguments about the location of the CS project file and the name of the Virtual Directory to setup. Currently, we’re only handling virtual directories, but going forward, we like to handle everything needed to get the system up and running an a local machine. Anyway, the file is checking for extension points in the build file using and XML read on the existing build target. This is because msbuild, as far as I could find, doesn’t have a target exists method. I thinking about removing these check, as I don’t think we’re going to end up using them like I initially thought.

However, the bulk of the file is in the **AspNetSetup target. It checks for a default IIS 6 web site and if found it deletes any existing virtual directory and re-creates it. This helps if you have different branches in different locations and want to quickly change the setup. </p>

Anyway, here’s the XML:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="CopyToOutput">

    <PropertyGroup>

        <MSBuildCommunityPackPath>....\ThirdParty.Dependencies\MSBuild.Community.Tasks<span style=“color: blue”></</span>MSBuildCommunityPackPath>

        <MSBuildCommunityTasksPath>.<span style=“color: blue”></</span>MSBuildCommunityTasksPath>

    </PropertyGroup>

    <Import Project="$(MSBuildCommunityPackPath)\MSBuild.Community.Tasks.Targets"/>

 

    <PropertyGroup>

        <MSBuildExtensionPackPath>....\ThirdParty.Dependencies\MSBuild Extension\ExtensionPack</MSBuildExtensionPackPath>

        <ExtensionTasksPath>.<span style=“color: blue”></</span>ExtensionTasksPath>

    </PropertyGroup>

    <Import Project="$(MSBuildExtensionPackPath)\MSBuild.ExtensionPack.tasks"/>

 

    <Import Project="$(ProjectFile)" Condition="$(ProjectFile) != ‘’"/>

    <ItemGroup>

        <ProjectPath Include="$(ProjectFile)"></ProjectPath>

    </ItemGroup>

 

    <PropertyGroup>

        <SetupDependsOnTargets>CoreBeforeSetup;CoreAspNetSetup;CoreAfterSetup</SetupDependsOnTargets>

    </PropertyGroup>

 

    <Target Name="Setup" DependsOnTargets="$(SetupDependsOnTargets)">

    </Target>

 

    <Target Name="CoreBeforeSetup">

        <Message Text="Setup: $(ProjectFile) types ‘$(ProjectTypeGuids)’" Importance="high" />

 

        <XmlRead Prefix="n"

                 Namespace="http://schemas.microsoft.com/developer/msbuild/2003"

                 XPath="/n:Project/n:Target[@Name=‘BeforeSetup’]/@Name"

                 XmlFileName="$(ProjectFile)">

            <Output TaskParameter="Value" PropertyName="FoundTargetName" />

        </XmlRead>

        <CallTarget Targets="BeforeSetup" Condition="$(FoundTargetName) != ‘’" />

    </Target>

 

    <Target Name="</strong>InitializeCoreAspNetSetup" Condition="‘@(AspNetWebsiteDirectories)’ == ‘’">

        <CreateItem

            Include="%(ProjectPath.RootDir)%(ProjectPath.Directory)"

            AdditionalMetadata="IISVirtualDirectory=$(IISVirtualDirectory)"

            Condition="Exists(‘$(OutputDirectory)$(AssemblyName)’) AND ‘$(IISVirtualDirectory)’ != ‘’">

            <Output TaskParameter="Include" ItemName="AspNetWebsiteDirectories" />

        </CreateItem>

    </Target>

 

    <Target Name="CoreAspNetSetup"

            DependsOnTargets="InitializeCoreAspNetSetup</span>"></p>

        <CallTarget Targets="</strong>AspNetSetup" Condition="‘@(AspNetWebsiteDirectories)’ != ‘’"/>

    </Target>

 

    <Target Name="__AspNetSetup">

        <Iis6Website TaskAction="CheckExists" Name="Default Web Site" >

            <Output PropertyName="Iis6Exists" TaskParameter="Exists"/>

        </Iis6Website>

 

        <Message Text="Website: %(AspNetWebsiteDirectories.IISVirtualDirectory) At %(AspNetWebsiteDirectories.FullPath)"

                 Importance="high"

                 Condition="$(Iis6Exists)"/>

 

        <WebDirectoryDelete VirtualDirectoryName="%(AspNetWebsiteDirectories.IISVirtualDirectory)"

                            ContinueOnError="true"

                            Condition="$(Iis6Exists)"/>

 

        <WebDirectoryCreate VirtualDirectoryName="%(AspNetWebsiteDirectories.IISVirtualDirectory)"

                            VirtualDirectoryPhysicalPath="%(AspNetWebsiteDirectories.FullPath)"

                            Condition="$(Iis6Exists)"/>

    </Target>

 

    <Target Name="CoreAfterSetup">

        <XmlRead Prefix="n"

                 Namespace="http://schemas.microsoft.com/developer/msbuild/2003"

                 XPath="/n:Project/n:Target[@Name=‘AfterSetup’]/@Name"

                 XmlFileName="$(ProjectFile)">

            <Output TaskParameter="Value" PropertyName="FoundTargetName" />

        </XmlRead>

        <CallTarget Targets="AfterSetup" Condition="$(FoundTargetName) != ‘’" />

    </Target>

</Project>

</p></div></p>


An Approach to Extending MSBuild

download

I’m working toward setting up a master build for our enterprise code using MSBuild. My first pass was to include a common build project in each of our csproj files. This was a bad idea for two reasons. One it’s a pain to update each csproj file. And two, VS complains when you open a csproj file with modifications. So, it was off to find a different approach, this was inspired by Sayed Ibrahim. The idea is to call your common build project file with the csproj file name and import that, giving you access to that files properties. One issue is the relative paths are now based on your common build projects location, this didn’t end up being to much of an issue. So anyway, here some code … err XML:

    <Target Name="CoreDeploy" DependsOnTargets="CoreBuild">

        <MSBuild Projects="SubProjects\Publish.proj" Targets="Deploy"

                 Properties="ProjectFile=%(ProjectsToBuild.FullPath);OutputDirectory=$(OutputRootDirectory);IISVirtualDirectory=%(ProjectsToBuild.IISVirtualDirectory)">

        </MSBuild>

    </Target>

</p>

The above calls into the Publish.proj file, which in turn includes the passed in ProjectFile.

 

    <Import Project="$(ProjectFile)" Condition="$(ProjectFile)!=‘’"/>

    <ItemGroup>

        <ProjectPath Include="$(ProjectFile)"></ProjectPath>

    </ItemGroup>

</p>

The above bit of code includes the project file in the Deploy.proj allowing it’s targets, properties, and items to be called.

Using the above approach I could have common setup project and a common deploy project, I’ll blog about these in following posts.


Brutalist Framework