Added EF to Repository and Integration tests project.
namespace Humour.Respository
{
public class HumourContext : DbContext
{
public HumourContext() : base("Humour") { }
public DbSet<Story> Stories { get; set; }
}
}
Making the db name Humour
EF Uses .\SQLEXPRES by default if no connection string is specified.
However want to use repositories and not call the context directly from external code.
To implement Unit of Work (that enabled you to make multiple changes to your data and submit them to the database all at once) the repos shouldn’t created instances of HumourContext themselves in each public method. Useful to have a factory.
Additionally it would be useful if the same HumourContext instance was used for the entire HTTP request ie can share same instance across multiple pieces of code running in the same request. Giving opportunity to treat multiple db updates as a single unit.
Building a Context Storage Mechanism
Implement a factory class that can create and return instances of HumourContext.
- DataContextFactory – static class with a static GetDataContext method (EF repositories project)
- DataContextStorageFactory - (infrastructure project as not tied to specific EF implementation)
- IDataConextStorageContainer
- HttpDataContextStorageContainer – web related projects
- ThreadDataContextStorageContainer – desktop apps and unit test projects
/// <summary>
/// Manages instances of the ContactManagerContext and stores them in an appropriate storage container.
/// </summary>
public static class DataContextFactory
{
/// <summary>
/// Clears out the current ContactManagerContext.
/// </summary>
public static void Clear()
{
var dataContextStorageContainer = DataContextStorageFactory<HumourContext>.CreateStorageContainer();
dataContextStorageContainer.Clear();
}
/// <summary>
/// Retrieves an instance of ContactManagerContext from the appropriate storage container or
/// creates a new instance and stores that in a container.
/// </summary>
/// <returns>An instance of ContactManagerContext.</returns>
public static HumourContext GetDataContext()
{
var dataContextStorageContainer = DataContextStorageFactory<HumourContext>.CreateStorageContainer();
var humourContext = dataContextStorageContainer.GetDataContext();
if (humourContext == null)
{
humourContext = new HumourContext();
dataContextStorageContainer.Store(humourContext);
}
return humourContext;
}
}
[TestMethod]
public void CanExecuteQueryAgainstDataContext()
{
string randomString = Guid.NewGuid().ToString().Substring(0, 25);
var context = DataContextFactory.GetDataContext();
var story = new Story
{
Title = "test",
Content = randomString,
DateCreated = DateTime.Now,
DateModified = DateTime.Now
};
context.Stories.Add(story);
context.SaveChanges();
var storyCheck = context.Stories.First(st => st.Content == randomString);
storyCheck.Should().NotBeNull();
}
Interesting way to generate a random string!
Configuring Model’s Business Rules
- Property level eg required field, max length etc..
- Object level eg copare 2 fields with each other
Property Level
Attributes or fluent API.
Could use OnModelCreating in EF to put on fluent API rules. A better way is EntityTypeConfiguration class.
/// <summary>
/// Configures the behavior for a person in the model and the database.
/// </summary>
public class StoryConfiguration : EntityTypeConfiguration<Story>
{
/// <summary>
/// Initializes a new instance of the PersonConfiguration class.
/// </summary>
public StoryConfiguration()
{
Property(x => x.Title).IsRequired().HasMaxLength(25);
Property(x => x.Content).IsRequired().HasMaxLength(2048);
}
}
and wired up through:
public class HumourContext : DbContext
{
public HumourContext() : base("Humour") { }
public DbSet<Story> Stories { get; set; }
/// <summary>
/// Configures the EF context.
/// </summary>
/// <param name="modelBuilder">The model builder that needs to be configured.</param>
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new StoryConfiguration());
}
}
Object Level Validation
/// <summary>
/// Determines whether this object is valid or not.
/// </summary>
/// <param name="validationContext">Describes the context in which a validation check is performed.</param>
/// <returns>A IEnumerable of ValidationResult. The IEnumerable is empty when the object is in a valid state.</returns>
public abstract IEnumerable<ValidationResult> Validate(ValidationContext validationContext);
/// <summary>
/// Determines whether this object is valid or not.
/// </summary>
/// <returns>A IEnumerable of ValidationResult. The IEnumerable is empty when the object is in a valid state.</returns>
public IEnumerable<ValidationResult> Validate()
{
var validationErrors = new List<ValidationResult>();
var ctx = new ValidationContext(this, null, null);
Validator.TryValidateObject(this, ctx, validationErrors, true);
return validationErrors;
}
DomainEntity implements DataAnnotations.IValidatableObject. So each entity must override Validate
Can’t do dynamic rules on attributes eg DateOfBirth can’t be later than today.
Calling validate on the Collections eg StoryCollection
/// <summary>
/// Validates this object. It validates dependencies between properties and also calls Validate on child collections;
/// </summary>
/// <param name="validationContext"></param>
/// <returns>A IEnumerable of ValidationResult. The IEnumerable is empty when the object is in a valid state.</returns>
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
yield break;
if (StoryType == StoryType.None)
{
yield return new ValidationResult("Type can't be None.", new[] { "Type" });
}
//if (DateOfBirth < DateTime.Now.AddYears(Constants.MaxAgePerson * -1))
//{
// yield return new ValidationResult("Invalid range for DateOfBirth; must be between today and 130 years ago.", new[] { "DateOfBirth" });
//}
//if (DateOfBirth > DateTime.Now)
//{
// yield return new ValidationResult("Invalid range for DateOfBirth; must be between today and 130 years ago.", new[] { "DateOfBirth" });
//}
foreach (var result in Votes.Validate())
{
yield return result;
}
Custom Validate method is not called as long as one of the Required or other attributes, or property-based rules set using the Fluent API are causing an objec to be invalid.
Add more tests to StoryTests class to test this validation
**problem – tests only work with attribute level validation.. if I put isRequired in StoryConfiguration tests don’t work
[TestMethod]
public void StoryWithTypeStoryTypeNoneIsInvalid()
{
var story = new Story();
story.StoryType = StoryType.None;
story.Validate().Count(x => x.MemberNames.Contains("StoryType")).Should().BeGreaterThan(0);
}
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
yield break;
if (StoryType == StoryType.None)
{
yield return new ValidationResult("StoryType can't be None.", new[] { "StoryType" });
}
Dealing With Database Initialisation
- Use a specialised version of DropCreateDatabaseIfModelChanges at dev time
- At prod time turn off database initialiser.. ie model is not checked
Have setup so that default data is there for integration tests. Can also override the seed method and put in own default data for tests.
/// <summary>
/// Used to initialize the HumourContext from Integration Tests
/// </summary>
public static class HumourContextInitializer
{
/// <summary>
/// Sets the IDatabaseInitializer for the application.
/// </summary>
/// <param name="dropDatabaseIfModelChanges">When true, uses the MyDropCreateDatabaseIfModelChanges to recreate the database when necessary.
/// Otherwise, database initialization is disabled by passing null to the SetInitializer method.
/// </param>
public static void Init(bool dropDatabaseIfModelChanges)
{
if (dropDatabaseIfModelChanges)
{
Database.SetInitializer(new MyDropCreateDatabaseIfModelChanges());
using (var db = new HumourContext())
{
db.Database.Initialize(false);
}
}
else
{
Database.SetInitializer<HumourContext>(null);
}
}
}
/// <summary>
/// A custom implementation of HumourContext that creates a new Story and Vote.
/// </summary>
public class MyDropCreateDatabaseIfModelChanges : DropCreateDatabaseIfModelChanges<HumourContext>
{
/// <summary>
/// Creates a new Story and Vote
/// </summary>
/// <param name="context">The context to which the new seed data is added.</param>
protected override void Seed(HumourContext context)
{
var story = new Story
{
Title = "Banana",
StoryType = StoryType.Joke,
Content = "asdf",
};
story.Votes.Add(CreateVote());
context.Stories.Add(story);
}
private static Vote CreateVote()
{
return new Vote { IPAddress = "192.168.1.1" };
}
}
then wired up to tests via:
dev web.config:
<contexts>
<context type="Humour.Repository.HumourContext, Humour.Repository" disableDatabaseInitialization="false">
<databaseInitializer type="Humour.Repository.MyDropCreateDatabaseIfModelChanges, Humour.Repository" />
</context>
</contexts>
prod:
<contexts>
<context type="Humour.Repository.HumourContext, Humour.Repository" disableDatabaseInitialization="true">
<!--<databaseInitializer type="Humour.Repository.MyDropCreateDatabaseIfModelChanges, Humour.Repository" />-->
</context>
</contexts>
Implementing a Base Repository Class
Abstract generic repository methods into a base
Search
public class StoryRepository : Repository<Story>, IStoryRepository
{
public IEnumerable<Story> FindByTitle(string title)
{
return DataContextFactory.GetDataContext().Set<Story>()
.Where(st => st.Title == title).ToList();
}
}
Unit Of Work
/// <summary>
/// Represents a unit of work
/// </summary>
public interface IUnitOfWork : IDisposable
{
/// <summary>
/// Commits the changes to the underlying data store.
/// </summary>
/// <param name="resetAfterCommit">When true, all the previously retrieved objects should be cleared from the underlying model / cache.</param>
void Commit(bool resetAfterCommit);
/// <summary>
/// Undoes all changes to the entities in the model.
/// </summary>
void Undo();
}
/// <summary>
/// Creates new instances of a unit of Work.
/// </summary>
public interface IUnitOfWorkFactory
{
/// <summary>
/// Creates a new instance of a unit of work
/// </summary>
IUnitOfWork Create();
/// <summary>
/// Creates a new instance of a unit of work
/// </summary>
/// <param name="forceNew">When true, clears out any existing in-memory data storage / cache first.</param>
IUnitOfWork Create(bool forceNew);
}
Concrete:
/// <summary>
/// Defines a Unit of Work using an EF DbContext under the hood.
/// </summary>
public class EFUnitOfWork : IUnitOfWork
{
/// <summary>
/// Initializes a new instance of the EFUnitOfWork class.
/// </summary>
/// <param name="forceNewContext">When true, clears out any existing data context first.</param>
public EFUnitOfWork(bool forceNewContext)
{
if (forceNewContext)
{
DataContextFactory.Clear();
}
}
/// <summary>
/// Saves the changes to the underlying DbContext.
/// </summary>
public void Dispose()
{
DataContextFactory.GetDataContext().SaveChanges();
}
/// <summary>
/// Saves the changes to the underlying DbContext.
/// </summary>
/// <param name="resetAfterCommit">When true, clears out the data context afterwards.</param>
public void Commit(bool resetAfterCommit)
{
DataContextFactory.GetDataContext().SaveChanges();
if (resetAfterCommit)
{
DataContextFactory.Clear();
}
}
/// <summary>
/// Undoes changes to the current DbContext by removing it from the storage container.
/// </summary>
public void Undo()
{
DataContextFactory.Clear();
}
}
/// <summary>
/// Creates new instances of an EF unit of Work.
/// </summary>
public class EFUnitOfWorkFactory : IUnitOfWorkFactory
{
/// <summary>
/// Creates a new instance of an EFUnitOfWork.
/// </summary>
public IUnitOfWork Create()
{
return Create(false);
}
/// <summary>
/// Creates a new instance of an EFUnitOfWork.
/// </summary>
/// <param name="forceNew">When true, clears out any existing data context from the storage container.</param>
public IUnitOfWork Create(bool forceNew)
{
return new EFUnitOfWork(forceNew);
}
}
and it working:
Because the UoW is based on interfaces it is easy to use in unit testable environments eg
public class PeopleController : BaseController
{
private readonly IPeopleRepository _peopleRepository;
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
const int PageSize = 10;
/// <summary>
/// Initializes a new instance of the PeopleController class.
/// </summary>
public PeopleController(IPeopleRepository peopleRepository, IUnitOfWorkFactory unitOfWorkFactory)
{
_peopleRepository = peopleRepository;
_unitOfWorkFactory = unitOfWorkFactory;
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(CreateAndEditPerson createAndEditPerson)
{
if (ModelState.IsValid)
{
try
{
using (_unitOfWorkFactory.Create())
{
Person person = new Person();
Mapper.Map(createAndEditPerson, person);
_peopleRepository.Add(person);
return RedirectToAction("Index");
}
}
catch (ModelValidationException mvex)
{
foreach (var error in mvex.ValidationErrors)
{
ModelState.AddModelError(error.MemberNames.FirstOrDefault() ?? "", error.ErrorMessage);
}
}
}
return View();
}
The controller receives an instance of IPeopleRepository and IUnitOfWorkFactory. Both parameters are based on interfaces so it’s easy to pass other types in during testing. At runtime we’ll use IoC.
This makes our app a lot easier to change as:
- Add property to Model class
- Use the field in the UI
Compared to having to modify SP’s.
Managing Relationships
Problem with orphaned records if we delete a Story – Votes wouldn’t be deleted.
Do this on an override at HumourContext
/// <summary>
/// Hooks into the Save process to get a last-minute chance to look at the entities and change them. Also intercepts exceptions and
/// wraps them in a new Exception type.
/// </summary>
/// <returns>The number of affected rows.</returns>
public override int SaveChanges()
{
// Need to manually delete all "owned objects" that have been removed from their owner, otherwise they'll be orphaned.
var orphanedObjects = ChangeTracker.Entries().Where(
e => (e.State == EntityState.Modified || e.State == EntityState.Added) &&
e.Entity is IHasOwner &&
e.Reference("Owner").CurrentValue == null);
foreach (var orphanedObject in orphanedObjects)
{
orphanedObject.State = EntityState.Deleted;
}
try
{
var modified = ChangeTracker.Entries().Where(e => e.State == EntityState.Modified || e.State == EntityState.Added);
foreach (DbEntityEntry item in modified)
{
var changedOrAddedItem = item.Entity as IDateTracking;
if (changedOrAddedItem != null)
{
if (item.State == EntityState.Added)
{
changedOrAddedItem.DateCreated = DateTime.Now;
}
changedOrAddedItem.DateModified = DateTime.Now;
}
}
return base.SaveChanges();
}
catch (DbEntityValidationException entityException)
{
var errors = entityException.EntityValidationErrors;
var result = new StringBuilder();
var allErrors = new List<ValidationResult>();
foreach (var error in errors)
{
foreach (var validationError in error.ValidationErrors)
{
result.AppendFormat("\r\n Entity of type {0} has validation error \"{1}\" for property {2}.\r\n", error.Entry.Entity.GetType().ToString(), validationError.ErrorMessage, validationError.PropertyName);
var domainEntity = error.Entry.Entity as DomainEntity<int>;
if (domainEntity != null)
{
result.Append(domainEntity.IsTransient() ? " This entity was added in this session.\r\n" : string.Format(" The Id of the entity is {0}.\r\n", domainEntity.Id));
}
allErrors.Add(new ValidationResult(validationError.ErrorMessage, new[] { validationError.PropertyName }));
}
}
throw new Humour.Infrastructure.ModelValidationException(result.ToString(), entityException, allErrors);
}
}
Implementing IDateTracking
See code above!
Improving Error Messages Generated
Rather cryptic error message.
Better message – showing type, ID and the reason.
See code above too for this!