Oct 29 2009

Learning Bourne – Tiered CRUD Part II

Category: AlexRobson @ 18:31

If the title confuses you, read about the Bourne Framework here. If you haven’t read the first part, I recommend checking that out first.

Objective – Implement CRUD Persistence
There’s a lot to go through so this blog post will be pretty heavy and longer than the previous. The concepts presented here will be very watered down, the implementation will be missing a lot of real-world details you would normally expect to see in a proper application. Why? I’m just trying to show the basics of using Bourne to get you started.

We’ll be adding a domain, DTOs, and a web service that handles our data access layer but in a very shallow and imperfect manner. I wanted to show how Bourne works with more separation and decoupling instead of just building a 2 tier app.

Avoid CRUD Service Layers
Ok, I really think it’s important to go ahead and say that I got burned in the recent past trying to ignore the expert advice of a lot of talented and experienced folks by trying to make data-access based services instead of services which are behavior or use-case driven. Not only that, but n-Tier applications like this one aren’t necessarily the best architecture either.

Step 1 – Create The Database
Can’t really persist anything without a database. Copy and past the script below into SSMS (connected to a local database server) and run it.

    if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK4D02B4FC8CDBBFD6]') AND parent_object_id = OBJECT_ID('Credits'))
alter table Credits  drop constraint FK4D02B4FC8CDBBFD6


    if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK4D02B4FC240E1F28]') AND parent_object_id = OBJECT_ID('Credits'))
alter table Credits  drop constraint FK4D02B4FC240E1F28


    if exists (select * from dbo.sysobjects where id = object_id(N'[Movie]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Movie]

    if exists (select * from dbo.sysobjects where id = object_id(N'Credits') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table Credits

    if exists (select * from dbo.sysobjects where id = object_id(N'[Actor]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Actor]

    create table [Movie] (
        Id BIGINT IDENTITY NOT NULL,
       Name NVARCHAR(128) not null,
       primary key (Id)
    )

    create table Credits (
        MovieId BIGINT not null,
       ActorId BIGINT not null
    )

    create table [Actor] (
        Id BIGINT IDENTITY NOT NULL,
       FirstName NVARCHAR(64) not null,
       LastName NVARCHAR(64) not null,
       primary key (Id)
    )

    alter table Credits 
        add constraint FK4D02B4FC8CDBBFD6 
        foreign key (ActorId) 
        references [Actor]

    alter table Credits 
        add constraint FK4D02B4FC240E1F28 
        foreign key (MovieId) 
        references [Movie]

Step 2a – Create The Domain
Add a class library project named crudDomain and add the following two clases to it:

public class Movie
{
    public virtual long Id { get; set; }
    public virtual string Name { get; set; }
    public virtual IList<Actor> Starring { get; set; }
}

public class Actor
{
    public virtual long Id { get; set; }
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual IList<Movie> StarredIn { get; set; }
}

Step 2b – Create The Domain Mappings
Next we’re going to add the fluent maps to the domain project using Fluent NHibernate. If you haven’t used Fluent NH yet, it’s one of two libraries that will open the gate to persistence nerdvana. Seriously. It’s just about the best thing I’ve used for mapping schema to domain. I’m not going to go into depth on this either because there’s already lots of information on Fluent NH.

public class MovieMap : ClassMap<Movie>
{
    public MovieMap()
    {
        Id(m => m.Id).GeneratedBy.Identity();
        Map(m => m.Name).Not.Nullable().Length(128);

        HasManyToMany(m => m.Starring)
            .Table("Credits")
            .ParentKeyColumn("MovieId")
            .ChildKeyColumn("ActorId")
            .Cascade.All();
    }
}

public class ActorMap : ClassMap<Actor>
{
    public ActorMap()
    {
        Id(a => a.Id).GeneratedBy.Identity();
        Map(a => a.FirstName).Not.Nullable().Length(64);
        Map(a => a.LastName).Not.Nullable().Length(64);

        HasManyToMany(a => a.StarredIn)
            .Table("Credits")
            .ParentKeyColumn("MovieId")
            .ChildKeyColumn("ActorId")
            .Cascade.All().Inverse();
    }
}

Step 3 – Create The DTOs In The Contracts Project
Remember how I said the crudContracts project was for service AND data contracts? Well, today we’re going to add a few:

[DataContract]
public class CastList
{
    [DataMember]
    public virtual long Id { get; set; }
    [DataMember]
    public virtual string Title { get; set; }
    [DataMember]
    public virtual IList<CastMember> Starring { get; set; }
}

[DataContract]
public class CastMember
{
    [DataMember]
    public virtual long Id { get; set; }
    [DataMember]
    public virtual string FirstName { get; set; }
    [DataMember]
    public virtual string LastName { get; set; }
}

[DataContract]
public class Movie
{
    [DataMember]
    public virtual long Id { get; set; }
    [DataMember]
    public virtual string Title { get; set; }
}

Step 4a – Create The CRUD Service Contract
Now we can add the service contract to our crudContracts project, create a file called IMovieService and paste in the following bit of CRUDdy goodness:

[ServiceContract]
public interface IMovieService
{
    [OperationContract]
    void AddMovie(string Title);
    [OperationContract]
    void AddCastMember(long movieId, CastMember castMember);
    [OperationContract]
    void RemoveCastMember(long movieId, long castMemberId);
    [OperationContract]
    CastList GetCastList(long movieId);
    [OperationContract]
    void UpdateMovie(Movie movie);
}

Step 4b – Implement The CRUD Service
Ok, now we’re getting to the part where Bourne starts making your life simpler. Remember how we added a dependency to our controller in the MVC side yesterday? Well, today we’ll add a dependency to a WCF service and show how Bourne manages most of the NHibernate concerns for you. Let’s start out by adding the MovieService class file to the crudServices project but I’m going to give you the service’s shell to start with and add code as we go:

public class MovieService : DTO.IMovieService
{
    public void AddMovie(string Title)
    {
        throw new NotImplementedException();
    }

    public void AddCastMember(long movieId, DTO.CastMember castMember)
    {
        throw new NotImplementedException();
    }

    public void RemoveCastMember(long movieId, long castMemberId)
    {
        throw new NotImplementedException();
    }

    public DTO.CastList GetCastList(long movieId)
    {
        throw new NotImplementedException();
    }

    public DTO.Movie GetMovie(long movieId)
    {
        throw new NotImplementedException();
    }

    public IList<DTO.Movie> GetMovies()
    {
        throw new NotImplementedException();
    }

    public void UpdateMovie(DTO.Movie movie)
    {
        throw new NotImplementedException();
    }
}

Note: I aliased my crudContracts namespace with DTO so that there would be less confusion between class names.

The first thing we’ll add is very boring (and arguably unnecessary with the right tools): we’re going to need a way to translate data between the domain model and DTO object. My utilitarian approach for this demo is through extension methods:

public static class Translator
    {
        public static DTO.Movie ToMovie(this Movie movie)
        {
            return new DTO.Movie()
                   {
                       Id = movie.Id,
                       Title = movie.Title
                   };
        }

        public static DTO.CastList ToCastList(this Movie movie)
        {
            return new DTO.CastList()
                   {
                       Id = movie.Id,
                       Title = movie.Title,
                       Starring = movie.Starring.Select(a => a.ToCastMember()).ToList()
                   };
        }

        public static DTO.CastMember ToCastMember(this Actor actor)
        {
            return new DTO.CastMember()
                   {
                       Id = actor.Id,
                       FirstName = actor.FirstName,
                       LastName = actor.LastName,
                   };
        }

        public static Movie FromDTO(this DTO.Movie movie)
        {
            return new Movie()
                   {
                       Id = movie.Id,
                       Title = movie.Title
                   };
        }

        public static Actor FromDTO(this DTO.CastMember castMember)
        {
            return new Actor()
                   {
                       FirstName = castMember.FirstName,
                       LastName = castMember.LastName
                   };
        }
    }

The Bourne Framework abstracts a good bit of the NHibernate API via the IRepository<> interface. There are a lot of reasons for this, but the take-away from this post is that it does actually simplify things quite a bit as well as provide some really interesting extension points. We’re going to add a dependency on IRepository<Movie> to our service next by pasting the following into MovieService:

private IRepository<Movie> _movieRepository;

public MovieService(IRepository<Movie> movieRepository)
{
    _movieRepository = movieRepository;
}

This is how Bourne will provide our IRepository instance to the service. Now we’ll look at how to implement our operations in the service using the IRepository instance. I’m not going to take these one at a time simply because the code should be self explanatory, but let’s talk about a few things really quick before looking at the code. First off, IRepository<> does a nice job exposing paging, ordering and filtering out of the box using LINQ but we’re not going to use them this go round (look for a future post). Second, I’m not exposing all the different calls for IRepository in this blog post either. Finally, I want to briefly explain the GetAll<>() call because the call may seem strange. Select is a helper class that exposes advanced filtering, paging and ordering. In our case, because we want all results, we’re using its All<>() method.

public MovieService(IRepository<Movie> movieRepository)
{
    _movieRepository = movieRepository;
}

public void AddMovie(string Title)
{
    _movieRepository.Insert(new Movie() { Title = Title});
}

public void AddCastMember(long movieId, DTO.CastMember castMember)
{
    var movie = _movieRepository.Get<Movie>(movieId);
    movie.AddCastMember(castMember.FromDTO());
}

public void RemoveCastMember(long movieId, long castMemberId)
{
    var movie = _movieRepository.Get<Movie>(movieId);
    movie.Starring.Remove(movie.Starring.First(a => a.Id == castMemberId));
}

public DTO.CastList GetCastList(long movieId)
{
    return _movieRepository.Get<Movie>(movieId).ToCastList();
}

public DTO.Movie GetMovie(long movieId)
{
    return _movieRepository.Get<Movie>(movieId).ToMovie();
}

public IList<DTO.Movie> GetMovies()
{
    return _movieRepository
        .GetAll(Select.All<Movie>())
        .Select(m => m.ToMovie())
        .ToList();
}

public void UpdateMovie(DTO.Movie movie)
{
    var oldMovie = _movieRepository.Get<Movie>(movie.Id);
    oldMovie.Title = movie.Title;
}

Note: our DTO types get passed in and/or are returned. At no point are any of the Domain types exposed. This is your “anti-corruption” layer. This allows us to adapt the conceptual model without immediately impacting our consumer or change how the application handles/requests the data without requiring the conceptual model to change.

Step 5 – Add The Service EndPoint and Configuration To The Service Host
The service endpoint will look just like the one we added in part one with the class and contract changed. The configuration for our MovieService will be a lot more complex this time, however. When we’re just shooting a simple string across the wire, I don’t need to worry about the binding configuration so much. But if I’m sending object graphs back and forth, I want to make sure I’ve provided a binding configuration that won’t blow up under load.

There are many ways that Bourne’s WCF service client configuration can be handled. In this case, I chose to put the binding metadata for the service client on the server so everything is in one place and my client’s behavior is dictated by the server. First let’s cover the EndPoint. Add Movie.svc to the crudServiceHost project. Change the markup to the following:

<%@ ServiceHost Language="C#" Debug="true" Factory="crudServiceHost.ServiceFactory" Service="crudServices.MovieService, crudServices" %>

The next step is to add the following configuration to Web.Config. First add this block to your <services> element:

<service behaviorConfiguration="crudServiceHost.MovieServiceBehavior"
  name="crudServices.MovieService">
  <endpoint address="" binding="wsHttpBinding" contract="crudContracts.IMovieService" bindingConfiguration="WSHttpBinding_IMovieService">
   <identity>
    <dns value="localhost" />
   </identity>
  </endpoint>
  <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"  />
 </service>
</services>

Last, we add the binding configuration I mentioned before. Since we didn’t have a bindings section before, just add the entire block after the behaviors close tag:

<bindings>
  <wsHttpBinding>
    <binding name="WSHttpBinding_IMovieService" closeTimeout="00:01:00"
 openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
 bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
 maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text"
 textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
      <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
       maxBytesPerRead="4096" maxNameTableCharCount="16384" />
      <reliableSession ordered="true" inactivityTimeout="00:10:00"
       enabled="false" />
     <security mode="Message">
        <transport clientCredentialType="Windows" proxyCredentialType="None"
         realm="">
        </transport>
        <message clientCredentialType="Windows" negotiateServiceCredential="true"
         algorithmSuite="Default" establishSecurityContext="true" />
      </security>
    </binding>
  </wsHttpBinding>
</bindings>

Step 6 – Provide The Fluent Configuration
The last thing we need to add to the crudServiceHost application is the additional configuration. Before we only needed to register GreeterService as the type for IGreet. This time around we’re adding the fluent configuration for FluentNHibernate via the Bourne ConfigureFramework() call. When we’re done the constructor for ServiceFactory should look like this:

public ServiceFactory()
{
    ConfigureFramework()
        .WithRepository(r => r
                        .Database(
                            MsSqlConfiguration.MsSql2008.ConnectionString
                            (c =>
                             c.Database("tieredCrud")
                                 .Server("localhost")
                                 .Username("service")
                                 .Password("p@ss")
                            ))
                        .Mappings(m => m.FluentMappings.AddFromAssemblyOf<Movie>())
        );

    ObjectFactory.Configure(
        config =>
        {
            config.ForRequestedType<IGreet>().TheDefaultIsConcreteType<Greeter>();
            config.ForRequestedType<IMovieService>().TheDefaultIsConcreteType<MovieService>();
            config.AddRegistry(new Log4NetRegistry("crudServiceHost.log4net.xml"));
        });
}

Step 7 – Add The Additional Configuration
Now we’re on to the MVC side. We’re going to add our service URLs as application values to the config. I did this primarily because as I was testing, the port assigned to my WCF service by WebDev would change. Putting the URL in the Web.Config let’s you generate a new client just by changing the URL.

<appSettings>
  <add key="greeterMEX" value="http://localhost:46192/greeter.svc?wsdl"/>
  <add key="contactMEX" value="http://localhost:46192/movie.svc?wsdl"/>
</appSettings>

Next, let’s update our the constructor in the Global.asax to include a call to ConfigureFramework and add the new service client:

public MvcApplication()
{
    ConfigureFramework();
    ObjectFactory.Configure(cfg =>
                             {
                                 cfg.AddRegistry(new ServiceClientRegistry());
                                 cfg.Scan(scan => scan.Assembly(typeof(HomeController).Assembly));
                             });
    ServiceClientFactory.RegisterConfigurationDelegate<IGreet>(c => c.MetadataExchangeAddress = ConfigurationManager.AppSettings["greeterMEX"]);
    ServiceClientFactory.RegisterConfigurationDelegate<IMovieService>(c => c.MetadataExchangeAddress = ConfigurationManager.AppSettings["contactMEX"]);
}

Step 8 – Create The Movie Controller

We’re going to add the MovieController to the MVC application and then look at the actions. Starting out, we’ll just have the controller, the constructor and it’s dependency on the service:

public class MovieController : Controller
{
    private IService<IMovieService> _service;

    public MovieController(IService<IMovieService> service)
    {
        _service = service;
    }
}

Now let’s add the actions. But first, I’ll enumerate what we’re adding. We want the ability to:

  • List all movies (Index)
  • Add a movie (Create)
  • Edit the movie name (Edit)
  • View the movies details specifically cast members (CastList)
  • Add a cast member (AddMember)
  • Remove a cast member(RemoveMember)

So without further ado, here’s the code:

public class MovieController : Controller
{
    private IService<IMovieService> _service;

    public MovieController(IService<IMovieService> service)
    {
        _service = service;
    }

    //
    // GET: /Movie/

    public ActionResult Index()
    {
        return View(_service.Call(s => s.GetMovies()));
    }

    public ActionResult Create()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Create(FormCollection form)
    {
        var movie = new Movie();
        UpdateModel(movie, new []{"Title"});
        _service.Call(s => s.AddMovie(movie.Title));
        return RedirectToAction("Index");
    }

    public ActionResult Edit(long Id)
    {
        var movie = _service.Call(s => s.GetMovie(Id));
        return View(movie);
    }

    [HttpPost]
    public ActionResult Edit(long id, FormCollection form)
    {
        var movie = new Movie() {Id = id};
        UpdateModel(movie, new [] {"Title"});
        _service.Call(s => s.UpdateMovie(movie));
        return RedirectToAction("Index");
    }

    public ActionResult CastList(long id)
    {
        var castList = _service.Call(s => s.GetCastList(id));
        return View(castList);
    }

    [HttpPost]
    public ActionResult AddMember(long id, FormCollection form)
    {
        var castMember = new CastMember();
        UpdateModel(castMember, new [] {"FirstName", "LastName"});
        _service.Call(s => s.AddCastMember(id, castMember));

        var castList = _service.Call(s => s.GetCastList(id));
        return RedirectToAction("CastList", new { id = id});
    }

    [HttpPost]
    public ActionResult RemoveMember(long id, long castMemberId)
    {
        _service.Call(s => s.RemoveCastMember(id, castMemberId));
        return RedirectToAction("CastList", new {id = id});
    }
}

Screen Shots Of The Views
This isn’t an MVC tutorial which means I’m not going to go into the extremely weak-sauce views I slapped together to make this all work. I will be posting the code. In the mean-time, here are screen shots of the views for each action.

image

image

image

image

I’m going to wrap this up for now. I’ll post an update later to the post with a link to the source. In the meantime, I’d really appreciate comments, questions or thoughts in general. Just be patient if your comment doesn’t appear immediately, I currently have moderation turned on in the blog due to the spam-bots built to target this blog software.

Tags:

blog comments powered by Disqus