Skip to content

Switching MvcMovie sample to LINQ2DynamoDB Walkthrough

scale-tone edited this page Apr 16, 2017 · 1 revision

Rick Anderson authored a great walkthrough for those, who (like me) is just learning the basics of ASP.Net MVC 4.

Let's move the MvcMovie sample project from that walkthrough to DynamoDB and MemcacheD using Linq2DynamoDb.DataContext with as few code changes as possible. We'll keep the Controller and scaffolded View code untouched and only replace the MovieDBContext with another implementation.

1. Get yourself a running instance of MemcacheD. In production this usually will be an AWS ElastiCache cluster. But for development purposes you would want to have a local instance of it on your development machine (you can use MemcacheD package for Windows, created by Nick Pirocanac, which includes a MemcacheD Manager tool for quickly configuring and running the cache instance, or even two, or more). If you don't have a MemcacheD instance, that's not a big problem - the steps below are still valid and the sample will still be working, a bit slower though.

2. Be sure to have the MvcMovie project from Rick's original walkthrough prepared and running. By now it stores the Movie entities in a SQL Server database and communicates with it via an Entity Framework data context. Open the project in Visual Studio, if it's not opened already.

3. If you have enabled Code First Migrations for your project, then disable it by removing the Migrations folder. This is because Code First Migrations are, of course, not supported by Linq2DynamoDb.DataContext and because you don't need them, as DynamoDB is schemaless.

4. Add reference to AWSSDK 2.0.2.0, Enyim.Caching and Linq2DynamoDb.DataContext to your project.

5. Specify EntityBase as the base class for the Movie entity in your Models folder (yes, Linq2DynamoDb.DataContext is still a bit obtrusive):

public class Movie : EntityBase
{
	// movie properties...
}

6. Add the following static class to your Models folder:

public static class MoviesDataTableExtensions
{
    public static void Add(this DataTable<Movie> table, Movie newMovie)
    {
        // This demonstrates the use of a table-wide lock.
        // The lock will be released automatically upon next submit
        table.AcquireTableLockTillSubmit("Id field lock", TimeSpan.FromSeconds(5));
        try
        {
            newMovie.ID = table.Max(m => m.ID) + 1;
        }
        catch (InvalidOperationException) // if no entities exist yet
        {
            newMovie.ID = 1;
        }
        table.InsertOnSubmit(newMovie);
    }

    public static void Remove<TEntity>(this DataTable<TEntity> table, TEntity newEntity)
    {
        table.RemoveOnSubmit(newEntity);
    }

    /// <summary>
    /// Creates a Movie table, if it doesn't exist yet. 
    /// Adds some initial data upon creation.
    /// it is essential to move this method's implementation out of 
    /// MovieDBContext class, to avoid deadlocks
    /// </summary>
    public static void CreateTablesIfTheyDoNotExist(this DataContext ctx)
    {
        ctx.CreateTableIfNotExists
        (
            new CreateTableArgs<Movie>
            (
                5, 5,
                m => m.ID,
                null, null,
                () => new[]()
                {
                    new Movie
                    {
                        ID = 1,
                        Title = "When Harry Met Sally",
                        ReleaseDate = DateTime.Parse("1989-1-11"),
                        Genre = "Romantic Comedy",
                        Price = 7.99M
                    },
                    new Movie
                    {
                        ID = 2,
                        Title = "Ghostbusters",
                        ReleaseDate = DateTime.Parse("1984-3-13"),
                        Genre = "Comedy",
                        Price = 8.99M
                    },
                    new Movie
                    {
                        ID = 3,
                        Title = "Ghostbusters 2",
                        ReleaseDate = DateTime.Parse("1986-2-23"),
                        Genre = "Comedy",
                        Price = 9.99M
                    },
                    new Movie
                    {
                        ID = 4,
                        Title = "Rio Bravo",
                        ReleaseDate = DateTime.Parse("1959-4-15"),
                        Genre = "Western",
                        Price = 3.99M
                    }
                }
            )
        );
    }
}

MoviesDataTableExtensions provides a couple of extenstion methods to make Linq2DynamoDb.DataContext compatible with Entity Framework's paradigm. It also contains the CreateTablesIfTheyDoNotExist() method, that implements the Movie table initial creation.

Note, how a table-wide lock is used to implement the generation of unique ID values.

7. Replace the code for MovieDBContext class with the following (don't forget to put your own AWS account credentials):

public class MovieDBContext : DataContext
{
    #region Tables

    public DataTable<Movie> Movies
    {
        get 
        { 
            return this.GetTable<Movie>(() => 
                new EnyimTableCache(CacheClient, TimeSpan.FromDays(1))); 
        }
    }

    #endregion

    #region Methods to be compatible with EF paradigm

    public void SaveChanges()
    {
        this.SubmitChanges();
    }

    public class EntityEntryStub
    {
        public EntityState State { get; set; }
    }

    public EntityEntryStub Entry(Movie entity)
    {
        this.Movies.InsertOnSubmit(entity);
        return new EntityEntryStub();
    }

    public void Dispose()
    {
        // nothing to do
    }

    #endregion

    #region Initialization stuff

    private const string TableNamePrefix = "MvcTest";

    private static readonly IAmazonDynamoDB DynamoDbClient;
    private static readonly MemcachedClient CacheClient;

    static MovieDBContext()
    {
        const string accessKey = "<your AWS access key>";
        const string secretKey = "<your AWS secret key>";

        // creating a MemcacheD client (it's thread-safe and can be reused)
        var cacheConfig = new MemcachedClientConfiguration 
        {
            Protocol = MemcachedProtocol.Text
        };
        cacheConfig.AddServer("localhost", 11211);
        CacheClient = new MemcachedClient(cacheConfig);

        // creating a DynamoDb client in some region (AmazonDynamoDBClient is 
        // thread-safe and can be reused)
        DynamoDbClient = new AmazonDynamoDBClient
        (
            accessKey, 
            secretKey,
            new AmazonDynamoDBConfig 
            { 
                RegionEndpoint = RegionEndpoint.USWest2, MaxErrorRetry = 6 
            }
        );

        // checking if the table needs to be created
        var ctx = new DataContext(DynamoDbClient, TableNamePrefix);
        ctx.CreateTablesIfTheyDoNotExist();
    }

    public MovieDBContext() : base(DynamoDbClient, TableNamePrefix)
    {
        // configure logging
        this.OnLog += s => Debug.WriteLine(s);
    }

    #endregion
}

8. Start the project and navigate to URL similar to the following: http://localhost:38132/movies/searchindex. In a couple of minutes an MvcTestMovie table will be created in USWest2 (Oregon) region, and you will see the SearchIndex page in your browser with some movies loaded from that table.

9. Try to specify some filter by Genre and/or Title and press the Filter button. The page reloads in about a second. Press the Filter button once again. The page reloads in about 10 milliseconds (if it still takes a second to reload, it means, that your MemcacheD instance is not started or not available). Now create a new movie and notice, that the filters you tried before are still loaded in milliseconds.

That's it. Now you've made the MvcMovie sample much more scalable and cheaper, yet still fast enough.