I have a base class called Entity:
public class Entity
{
public int Id {get;set;}
}
Let's say I have a class called Customer:
public class Customer : Entity
{
public string Name {get;set;}
}
Now, using convention based mapping by code in NHibernate 3.3.1, I try the following:
public static class DataHelper
{
private static HbmMapping GetMappings()
{
var mapper = new CustomModelMapper(typeof(Entity));
return mapper.CompileMappingFor(
typeof(DataHelper).Assembly.GetExportedTypes()
.Where(x => x.IsSubclassOf(typeof(Entity))));
}
}
When I try to run my app, I get the error "Cannot extend unmapped class: Entity". I don't want to map the Entity class - it's just a base class for inheriting some common properties. How can I tell NHibernate to ignore the unmapped class? For reference, my CustomModelMapper class is listed below.
The code for my CustomModelMapper class is listed below for reference
internal class CustomModelMapper : ConventionModelMapper
{
private const int DEFAULT_STRING_LENGTH = 100;
private Type baseType;
public CustomModelMapper(Type baseType)
{
this.baseType = baseType;
}
public CustomModelMapper()
{
SetupInspectors();
}
protected override void AppendDefaultEvents()
{
base.AppendDefaultEvents();
BeforeMapClass += OnBeforeMapClass;
BeforeMapProperty += OnBeforeMapProperty;
BeforeMapManyToOne += OnBeforeMapManyToOne;
BeforeMapBag += OnBeforeMapBag;
BeforeMapList += OnBeforeMapList;
BeforeMapSet += OnBeforeMapSet;
}
protected void OnBeforeMapClass(IModelInspector modelInspector, Type type, IClassAttributesMapper classCustomizer)
{
classCustomizer.Id(type.GetProperty("Id"), m => m.Generator(Generators.Native));
}
protected void OnBeforeMapProperty(IModelInspector modelInspector, PropertyPath member, IPropertyMapper propertyCustomizer)
{
if (member.LocalMember.GetPropertyOrFieldType().IsEnum)
{
var type = member.LocalMember.GetPropertyOrFieldType();
var genericType = typeof(EnumStringType<>).MakeGenericType(type);
propertyCustomizer.Type(genericType, null);
}
if (member.LocalMember.GetPropertyOrFieldType() == typeof(string))
propertyCustomizer.Length(DEFAULT_STRING_LENGTH);
}
protected void OnBeforeMapManyToOne(IModelInspector modelInspector, PropertyPath member, IManyToOneMapper propertyCustomizer)
{
propertyCustomizer.Cascade(Cascade.All);
propertyCustomizer.Fetch(FetchKind.Join);
propertyCustomizer.Lazy(LazyRelation.NoLazy);
propertyCustomizer.Index(string.Format("IX{0}{1}",
member.GetContainerEntity(modelInspector).Name,
member.LocalMember.Name));
}
protected void OnBeforeMapBag(IModelInspector modelInspector, PropertyPath member, IBagPropertiesMapper propertyCustomizer)
{
propertyCustomizer.Cascade(Cascade.All);
propertyCustomizer.Lazy(CollectionLazy.Extra);
propertyCustomizer.Fetch(CollectionFetchMode.Subselect);
}
protected void OnBeforeMapList(IModelInspector modelInspector, PropertyPath member, IListPropertiesMapper propertyCustomizer)
{
propertyCustomizer.Cascade(Cascade.All);
propertyCustomizer.Lazy(CollectionLazy.Extra);
propertyCustomizer.Fetch(CollectionFetchMode.Subselect);
}
protected void OnBeforeMapSet(IModelInspector modelInspector, PropertyPath member, ISetPropertiesMapper propertyCustomizer)
{
propertyCustomizer.Cascade(Cascade.All);
propertyCustomizer.Lazy(CollectionLazy.Extra);
propertyCustomizer.Fetch(CollectionFetchMode.Subselect);
}
protected void SetupInspectors()
{
IsRootEntity((type, declared) =>
{
return baseType.Equals(type.BaseType);
});
IsEntity((type, declared) =>
{
return baseType.IsAssignableFrom(type) && !type.IsInterface;
});
IsVersion((member, declared) =>
{
return
member.Name == "Version" &&
member.MemberType == MemberTypes.Property &&
member.GetPropertyOrFieldType() == typeof(int);
});
IsBag((member, declared) =>
{
if (member.GetPropertyOrFieldType().IsGenericType)
return IsGenericType(member, typeof(ICollection<>));
return false;
});
IsList((member, declared) =>
{
if (member.GetPropertyOrFieldType().IsGenericType)
return IsGenericType(member, typeof(IList<>));
return false;
});
IsSet((member, declared) =>
{
if (member.GetPropertyOrFieldType().IsGenericType)
return IsGenericType(member, typeof(ICG.ISet<>));
return false;
});
}
protected static bool IsGenericType(MemberInfo member, Type targetType)
{
var type = member.GetPropertyOrFieldType();
var generics = type.GetGenericInterfaceTypeDefinitions();
return generics.Contains(targetType);
}
}
The problem is probably with your IsEntity convention. Currently, it will return true for Entity class itself. Just add another check:
IsEntity((type, declared) =>
{
return baseType.IsAssignableFrom(type) && !type.IsInterface &&
type != typeof(Entity); // <- skip Entity class
});
Edit
Also, you have two constructors in your CustomModelMapper class. One of them accepts base type, the other one is default and calls SetupInspectors(). As I can see, your default constructor will never be called, since you are calling the one that accepts the base type, and it doesn't call the default constructor...
And the consequence of that is... your SetupInspectors() method will also never be called.
Related
I have WebAPI (.NET Core) and use FluentValidator to validate model, including updating.
I use PATCH verb and have the following method:
public IActionResult Update(int id, [FromBody] JsonPatchDocument<TollUpdateAPI> jsonPatchDocument)
{
also, I have a validator class:
public class TollUpdateFluentValidator : AbstractValidator<TollUpdateAPI>
{
public TollUpdateFluentValidator ()
{
RuleFor(d => d.Date)
.NotNull().WithMessage("Date is required");
RuleFor(d => d.DriverId)
.GreaterThan(0).WithMessage("Invalid DriverId");
RuleFor(d => d.Amount)
.NotNull().WithMessage("Amount is required");
RuleFor(d => d.Amount)
.GreaterThanOrEqualTo(0).WithMessage("Invalid Amount");
}
}
and map this validator in Startup class:
services.AddTransient<IValidator<TollUpdateAPI>, TollUpdateFluentValidator>();
but it does not work. How to write valid FluentValidator for my task?
You will need to trigger the validation manually.
Your action method will be somthing like this:
public IActionResult Update(int id, [FromBody] JsonPatchDocument<TollUpdateAPI> jsonPatchDocument)
{
// Load your db entity
var myDbEntity = myService.LoadEntityFromDb(id);
// Copy/Map data to the entity to patch using AutoMapper for example
var entityToPatch = myMapper.Map<TollUpdateAPI>(myDbEntity);
// Apply the patch to the entity to patch
jsonPatchDocument.ApplyTo(entityToPatch);
// Trigger validation manually
var validationResult = new TollUpdateFluentValidator().Validate(entityToPatch);
if (!validationResult.IsValid)
{
// Add validation errors to ModelState
foreach (var error in validationResult.Errors)
{
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
// Patch failed, return 422 result
return UnprocessableEntity(ModelState);
}
// Map the patch to the dbEntity
myMapper.Map(entityToPatch, myDbEntity);
myService.SaveChangesToDb();
// So far so good, patch done
return NoContent();
}
You can utilize a custom rule builder for this. It might not be the most elegant way of handling it but at least the validation logic is where you expect it to be.
Say you have the following request model:
public class CarRequestModel
{
public string Make { get; set; }
public string Model { get; set; }
public decimal EngineDisplacement { get; set; }
}
Your Validator class can inherit from the AbstractValidator of JsonPatchDocument instead of the concrete request model type.
The fluent validator, on the other hand, provides us with decent extension points such as the Custom rule.
Combining these two ideas you can create something like this:
public class Validator : AbstractValidator<JsonPatchDocument<CarRequestModel>>
{
public Validator()
{
RuleForEach(x => x.Operations)
.Custom(HandleInternalPropertyValidation);
}
private void HandleInternalPropertyValidation(JsonPatchOperation property, CustomContext context)
{
void AddFailureForPropertyIf<T>(
Expression<Func<T, object>> propertySelector,
JsonPatchOperationType operation,
Func<JsonPatchOperation, bool> predicate, string errorMessage)
{
var propertyName = (propertySelector.Body as MemberExpression)?.Member.Name;
if (propertyName is null)
throw new ArgumentException("Property selector must be of type MemberExpression");
if (!property.Path.ToLowerInvariant().Contains(propertyName.ToLowerInvariant()) ||
property.Operation != operation) return;
if (predicate(property)) context.AddFailure(propertyName, errorMessage);
}
AddFailureForPropertyIf<CarRequestModel>(x => x.Make, JsonPatchOperationType.remove,
x => true, "Car Make cannot be removed.");
AddFailureForPropertyIf<CarRequestModel>(x => x.EngineDisplacement, JsonPatchOperationType.replace,
x => (decimal) x.Value < 12m, "Engine displacement must be less than 12l.");
}
}
In some cases, it might be tedious to write down all the actions that are not allowed from the domain perspective but are defined in the JsonPatch RFC.
This problem could be eased by defining none but rules which would define the set of operations that are valid from the perspective of your domain.
Realization bellow allow use IValidator<Model> inside IValidator<JsonPatchDocument<Model>>, but you need create model with valid properties values.
public class ModelValidator : AbstractValidator<JsonPatchDocument<Model>>
{
public override ValidationResult Validate(ValidationContext<JsonPatchDocument<Model>> context)
{
return _validator.Validate(GetRequestToValidate(context));
}
public override Task<ValidationResult> ValidateAsync(ValidationContext<JsonPatchDocument<Model>> context, CancellationToken cancellation = default)
{
return _validator.ValidateAsync(GetRequestToValidate(context), cancellation);
}
private static Model GetRequestToValidate(ValidationContext<JsonPatchDocument<Model>> context)
{
var validModel = new Model()
{
Name = nameof(Model.Name),
Url = nameof(Model.Url)
};
context.InstanceToValidate.ApplyTo(validModel);
return validModel;
}
private class Validator : AbstractValidator<Model>
{
/// <inheritdoc />
public Validator()
{
RuleFor(r => r.Name).NotEmpty();
RuleFor(r => r.Url).NotEmpty();
}
}
private static readonly Validator _validator = new();
}
You may try the below generic validator - it validates only updated properties:
public class JsonPatchDocumentValidator<T> : AbstractValidator<JsonPatchDocument<T>> where T: class, new()
{
private readonly IValidator<T> _validator;
public JsonPatchDocumentValidator(IValidator<T> validator)
{
_validator = validator;
}
private static string NormalizePropertyName(string propertyName)
{
if (propertyName[0] == '/')
{
propertyName = propertyName.Substring(1);
}
return char.ToUpper(propertyName[0]) + propertyName.Substring(1);
}
// apply path to the model
private static T ApplyPath(JsonPatchDocument<T> patchDocument)
{
var model = new T();
patchDocument.ApplyTo(model);
return model;
}
// returns only updated properties
private static string[] CollectUpdatedProperties(JsonPatchDocument<T> patchDocument)
=> patchDocument.Operations.Select(t => NormalizePropertyName(t.path)).Distinct().ToArray();
public override ValidationResult Validate(ValidationContext<JsonPatchDocument<T>> context)
{
return _validator.Validate(ApplyPath(context.InstanceToValidate),
o => o.IncludeProperties(CollectUpdatedProperties(context.InstanceToValidate)));
}
public override async Task<ValidationResult> ValidateAsync(ValidationContext<JsonPatchDocument<T>> context, CancellationToken cancellation = new CancellationToken())
{
return await _validator.ValidateAsync(ApplyPath(context.InstanceToValidate),
o => o.IncludeProperties(CollectUpdatedProperties(context.InstanceToValidate)), cancellation);
}
}
it has to be registered manually:
builder.Services.AddScoped<IValidator<JsonPatchDocument<TollUpdateAPI>>, JsonPatchDocumentValidator<TollUpdateAPI>>();
I am getting the following exception:
"None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'NetCore.DAL.EF.Repositories.Core.Common.SystemSettingRepository' can be invoked with the available services and parameters:\r\nCannot resolve parameter 'NetCore.DAL.EF.DatabaseFactory1[NetCore.DAL.EF.AppDbContext] databaseFactory' of constructor 'Void .ctor(NetCore.DAL.EF.DatabaseFactory1[NetCore.DAL.EF.AppDbContext])'."
Here is the Autofac registration info for SystemSettingRepository:
autofacServiceProvider.Non-Public members._lifetimeScope.ComponentRegistry.Registrations[28] = {Activator = SystemSettingRepository (ReflectionActivator), Services = [Dcs.NetCore.ApplicationCore.BLL.Domain.Features.Common.SystemSettings.ISystemSettingRepository, Dcs.NetCore.Infrastructure.Common.IRepository`1[[Dcs.NetCore.ApplicationCore.BLL.Domain.Entities.Common.SystemSetting, Dcs.NetCore.ApplicationCore.BLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], NetCore.DAL.EF.Repositories.Core.Common.SystemSettingRepository], Lifetime = Autofac.Core.Lifetime.CurrentScopeLifetime, Sharing = None, Ownership = OwnedByLifetimeScope}
autofacServiceProvider.Non-Public members._lifetimeScope.ComponentRegistry.Registrations[28].Target.Services[0].ServiceType = {Dcs.NetCore.ApplicationCore.BLL.Domain.Features.Common.SystemSettings.ISystemSettingRepository}
autofacServiceProvider.Non-Public members._lifetimeScope.ComponentRegistry.Registrations[28].Target.Services[1].ServiceType = {Dcs.NetCore.Infrastructure.Common.IRepository`1[Dcs.NetCore.ApplicationCore.BLL.Domain.Entities.Common.SystemSetting]}
autofacServiceProvider.Non-Public members._lifetimeScope.ComponentRegistry.Registrations[28].Target.Services[2].ServiceType = {NetCore.DAL.EF.Repositories.Core.Common.SystemSettingRepository}
As you can see, the services are registered. However, the databaseFactory parameter is not. Here is my code:
public class Startup
{
…
public IServiceProvider ConfigureServices(IServiceCollection services)
{
…
return AutofacBuilder();
}
private AutofacServiceProvider AutofacBuilder()
{
var builder = new ContainerBuilder();
builder.RegisterModule<AutofacModule_AspNetCore>();
builder.Populate(_services);
this.AutofacContainer = builder.Build();
var autofacServiceProvider = new AutofacServiceProvider(this.AutofacContainer);
return autofacServiceProvider;
}
}
public class AutofacModule_AspNetCore : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
RegisterDbContexts();
RegisterComponents();
}
private void RegisterComponents()
{
_builder.RegisterGeneric(typeof(DatabaseFactory<>))
.As(typeof(IDatabaseFactory<>))
.InstancePerDependency();
_builder.RegisterAssemblyTypes(_assembly)
.AsClosedTypesOf(typeof(IRepository<>))
.WithParameter(new ResolvedParameter((p, i) => p.Name == "databaseFactory",
(p, i) => i.Resolve<DatabaseFactory<AppDbContext>>()))
//.WithParameter("databaseFactory", new DatabaseFactory<AppDbContext>())
//.WithParameter(ResolvedParameter//.ForNamed<IDbContext>("coreDomainDbContext"));
//.WithParameter("databaseFactory", )
.InstancePerDependency();
}
private void RegisterDbContexts()
{
RegisterAppDbContextInstance();
}
private void RegisterAppDbContextInstance()
{
// register DbContextOptionsBuilderHelper
_builder.RegisterType<AppDbContextOptionsBuilderHelper>()
.InstancePerDependency();
// configure DbContextOptions
var dbContextOptionsBuilderHelper = new AppDbContextOptionsBuilderHelper();
var dbContextOptionsBuilder = dbContextOptionsBuilderHelper.GetDbContextOptionsBuilder();
// register DbContext
_builder.RegisterType<AppDbContext>()
.WithParameter("options", dbContextOptionsBuilder.Options)
.InstancePerDependency();
}
}
public class DatabaseFactory<T> : Disposable, IDatabaseFactory<T>
where T : DbContext //, new()
{
private T _dbContext;
private AppDbContextOptionsBuilderHelper _appDbContextOptionsBuilderHelper;
private DefaultDbContextOptionsBuilderHelper _defaultDbContextOptionsBuilderHelper;
public DatabaseFactory()
{
this._appDbContextOptionsBuilderHelper = new AppDbContextOptionsBuilderHelper(); // TODO: refactor to ctor injection
this._defaultDbContextOptionsBuilderHelper = new DefaultDbContextOptionsBuilderHelper(); // TODO: refactor to ctor injection
}
public T Get()
{
if (_dbContext == null)
Create();
return _dbContext;
}
private void Create()
{
switch (typeof(T).Name)
{
case "AppDbContext":
{
CreateAppDbContext();
break;
}
case "DefaultDbContext":
{
CreateDefaultDbContext();
break;
}
}
}
private void CreateAppDbContext()
{
var dbContextOptionsBuilder = _appDbContextOptionsBuilderHelper.GetDbContextOptionsBuilder();
this._dbContext = new AppDbContext(dbContextOptionsBuilder.Options) as T;
}
//protected override void DisposeCore()
//{
//
// cref: Autofac.Util.Disposable
//
// //TODO: should I override Autofac.Util.Disposable here?
// var msg = "DatabaseFactory.DisposeCore() executing";
// if (_dbContext != null)
// _dbContext.Dispose();
//}
}
public partial class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{ }
public AppDbContext()
{}
}
public class SystemSettingRepository : RepositoryBase<SystemSetting, AppDbContext>, ISystemSettingRepository
{
public SystemSettingRepository(DatabaseFactory<AppDbContext> databaseFactory)
: base(databaseFactory)
{ }
}
public interface ISystemSettingRepository : IRepository<SystemSetting>
{}
public partial interface IRepository<T> where T : class
{}
public abstract class RepositoryBase<T, U> : IRepositoryBase<T, U>
where T : class
where U : DbContext //, new()
{
private U _dbContext;
private readonly DbSet<T> _dbset;
protected RepositoryBase(DatabaseFactory<U> databaseFactory)
{
DatabaseFactory = databaseFactory;
_dbset = DataContext.Set<T>();
}
protected virtual DatabaseFactory<U> DatabaseFactory
{
get;
private set;
}
protected virtual U DataContext
{
get { return _dbContext = DatabaseFactory.Get(); }
}
public virtual async Task<T> AddAsync(T dao)
{
EntityEntry<T> entityEntry = _dbset.Add(dao);
var result = await SaveChangesAsync();
if (result > 0 && entityEntry != null && entityEntry.Entity != null)
return entityEntry.Entity;
else
return null;
}
}
public interface IRepositoryBase<T, U>
where T : class
where U : DbContext //, new()
{ }
The issue is caused by the way of registration of DatabaseFactory<>. This type is registered as an interface IDatabaseFactory<>. But it is resolved as itself in lambda argument of method WithParameter() on registration of repositories:
_builder.RegisterAssemblyTypes(_assembly)
.AsClosedTypesOf(typeof(IRepository<>))
.WithParameter(new ResolvedParameter((p, i) => p.Name == "databaseFactory",
// resolving type it self
// while it was registered as interface
(p, i) => i.Resolve<DatabaseFactory<AppDbContext>>()))
Autofac doesn't know how to resolve it, because it doesn't have corresponding registration. To make your code work you can resolve DatabaseFactory as an inteface like this:
.WithParameter(new ResolvedParameter((p, i) => p.Name == "databaseFactory",
(p, i) => i.Resolve<IDatabaseFactory<AppDbContext>>()))
Or add AsSelf() call to registration:
_builder.RegisterGeneric(typeof(DatabaseFactory<>))
.As(typeof(IDatabaseFactory<>))
// Register also as DatabaseFactory<>
.AsSelf()
.InstancePerDependency();
Based on Jimmy's Enumeration classes idea I am wanting to see if I can avoid using the constructor to instantiate my type (which I assume is happening with the discriminator-value) but rather use a "factory method"-esque way of getting my instance mapped from the db.
Here is my type:
public class Impact : Enumeration
{
public static readonly Impact Carbon
= new Impact(1, "Carbon dioxide equivalent", CommonUnit.CO2e);
public static readonly Impact Energy
= new Impact(2, "Energy", CommonUnit.MJ);
public static readonly Impact Cost
= new Impact(3, "Cost", CommonUnit.Dollars);
public Impact(int index, string name, CommonUnit unit)
: base(index, name)
{
this.Unit = unit;
}
public CommonUnit Unit { get; private set; }
}
And here is the definition for Enumeration:
public class Enumeration : ValueObject
{
public Enumeration(int index, string displayName)
{
this.Index = index;
this.DisplayName = displayName;
}
public int Index { get; private set; }
public string DisplayName { get; private set; }
public override string ToString()
{
return this.DisplayName;
}
public static IEnumerable<T> GetAllFor<T>() where T : Enumeration
{
foreach (var publicStatic in typeof(T).GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly))
{
Enumeration item = null;
item = (Enumeration)publicStatic.GetValue(null);
yield return item as T;
}
}
public static T With<T>(int index) where T : Enumeration
{
return GetAllFor<T>().SingleOrDefault(i => i.Index == index);
}
}
ValueObject simply covers off Equality functionality.
Elsewhere I use the static methods to get items from this enum (kinda like how you could use the core Enumeration static methods):
impact = Impact.With<Impact>(index.ImpactId.Value);
This is pretty handy but I want to know if I can get NHibernate to do this too when rehydrating objects.
Can it be done and how?
With an NHibernate Custom Type:
public class EnumerationType<T> : PrimitiveType where T : Enumeration
{
public EnumerationType()
: base(new SqlType(DbType.Int32))
{
}
public override object Get(IDataReader rs, int index)
{
object o = rs[index];
var value = Convert.ToInt32(o);
return Enumeration.With<T>(value);
}
public override object Get(IDataReader rs, string name)
{
int ordinal = rs.GetOrdinal(name);
return Get(rs, ordinal);
}
public override Type ReturnedClass
{
get { return typeof(T); }
}
public override object FromStringValue(string xml)
{
return int.Parse(xml);
}
public override string Name
{
get { return "Enumeration"; }
}
public override void Set(IDbCommand cmd, object value, int index)
{
var parameter = (IDataParameter)cmd.Parameters[index];
var val = (Enumeration)value;
parameter.Value = val.Value;
}
public override string ObjectToSQLString(object value, Dialect dialect)
{
return value.ToString();
}
public override Type PrimitiveClass
{
get { return typeof(int); }
}
public override object DefaultValue
{
get { return 0; }
}
}
If you're doing an HBM.xml-based mapping, you can set the custom type like this:
<property name="Impact" column="Impact" type="Namespace.To.EnumerationType`1[[Impact, AssemblyWithDomainEnum]], AssemblyWithNHibCustomType"/>
Alternatively, if you're using Fluent NHibernate, you can create a convention to map all enumeration types without having to configure each one individually:
public class EnumerationTypeConvention : IPropertyConvention, IPropertyConventionAcceptance
{
private static readonly Type _openType = typeof(EnumerationType<>);
public void Apply(IPropertyInstance instance)
{
var closedType = _openType.MakeGenericType(instance.Property.PropertyType);
instance.CustomType(closedType);
}
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => typeof(Enumeration).IsAssignableFrom(x.Property.PropertyType));
}
}
And then add that convention however you like in your Fluent NHibernate configuration.
This seemed to work too, but perhaps Jimmy's way seems easier:
public class ImpactEnumType : IUserType
{
public SqlType[] SqlTypes
{
get
{
//We store our Impact in a single column in the database that can contain a int (for the index value)
SqlType[] types = new SqlType[1];
types[0] = new SqlType(DbType.Int32);
return types;
}
}
public Type ReturnedType
{
get { return typeof(Impact); }
}
public bool Equals(object x, object y)
{
// Impact is derived from ValueObject which implements Equals
return x.Equals(y);
}
public int GetHashCode(object x)
{
// as above
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
//We get the string from the database using the NullSafeGet used to get ints
int impactIndex = (int)NHibernateUtil.Int32.NullSafeGet(rs, names[0]);
// then pull the instance from the Enumeration type using the static helpers
return Impact.With<Impact>(impactIndex);
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
//Set the value using the NullSafeSet implementation for int from NHibernateUtil
if (value == null)
{
NHibernateUtil.Int32.NullSafeSet(cmd, null, index);
return;
}
value = (value as Impact).Index;
NHibernateUtil.Int32.NullSafeSet(cmd, value, index);
}
public object DeepCopy(object value)
{
//We deep copy the Impact by creating a new instance with the same contents
if (value == null) return null;
return Impact.With<Impact>((value as Impact).Index);
}
public bool IsMutable
{
get { return false; }
}
public object Replace(object original, object target, object owner)
{
//As our object is immutable we can just return the original
return original;
}
public object Assemble(object cached, object owner)
{
//Used for casching, as our object is immutable we can just return it as is
return cached;
}
public object Disassemble(object value)
{
//Used for casching, as our object is immutable we can just return it as is
return value;
}
}
My HBM XML:
<property name="Impact" column="ImpactIndex" type="namespace.childnamespace.ImpactEnumType, namespace.childnamespace" />
I'm trying to implement an IUserType for states and country codes that will allow me to access both the two-letter code (what's stored in the database) as well as the full name. I'm following the example in the NHibernate 3.0 Cookbook (p. 225), but my problem is that my StreetAddress class is currently mapped as a component in my automapping configuration:
public override bool IsComponent(Type type)
{
return type == typeof(StreetAddress);
}
With this class identified as a component, I don't know how I can use an IUserType for the component class's property, since that class isn't explicitly mapped. There's nowhere that I could tell fluent NHibernate to use the IUserType specification.
#Firo was close, but there turned out to be a much easier solution. There were two steps here. First, I had to tell Fluent NHibernate not to map the State and Country classes, which reside in my domain layer:
public override bool ShouldMap(Type type)
{
return type.Name != "State" && type.Name != "Country";
}
Next, I simply had to create the conventions for the IUserType classes. This turned out to be easier than #Firo suggested:
public class CountryUserTypeConvention : UserTypeConvention<CountryType>
{
}
public class StateUserTypeConvention : UserTypeConvention<StateType>
{
}
The definition of those IUserTypes was pulled out of the cookbook referenced in the original question, but in case you don't want to read it:
public class CountryType : GenericWellKnownInstanceType<Country, string>
{
// The StateType is pretty much the same thing, only it uses "StateCode" instead of "CountryCode"
private static readonly SqlType[] sqlTypes =
new[] {SqlTypeFactory.GetString(2)};
public CountryType()
: base(new Countries(),
(entity, id) => entity.CountryCode == id,
entity => entity.CountryCode)
{
}
public override SqlType[] SqlTypes
{
get { return sqlTypes; }
}
}
And that derives from GenericWellKnownInstanceType:
[Serializable]
public abstract class GenericWellKnownInstanceType<T, TId> :
IUserType where T : class
{
private Func<T, TId, bool> findPredicate;
private Func<T, TId> idGetter;
private IEnumerable<T> repository;
protected GenericWellKnownInstanceType(
IEnumerable<T> repository,
Func<T, TId, bool> findPredicate,
Func<T, TId> idGetter)
{
this.repository = repository;
this.findPredicate = findPredicate;
this.idGetter = idGetter;
}
public Type ReturnedType
{
get { return typeof (T); }
}
public bool IsMutable
{
get { return false; }
}
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (ReferenceEquals(null, x) ||
ReferenceEquals(null, y))
{
return false;
}
return x.Equals(y);
}
public int GetHashCode(object x)
{
return (x == null) ? 0 : x.GetHashCode();
}
public object NullSafeGet(IDataReader rs,
string[] names, object owner)
{
int index0 = rs.GetOrdinal(names[0]);
if (rs.IsDBNull(index0))
{
return null;
}
var value = (TId) rs.GetValue(index0);
return repository.FirstOrDefault(x =>
findPredicate(x, value));
}
public void NullSafeSet(IDbCommand cmd,
object value, int index)
{
if (value == null)
{
((IDbDataParameter) cmd.Parameters[index])
.Value = DBNull.Value;
}
else
{
((IDbDataParameter) cmd.Parameters[index])
.Value = idGetter((T) value);
}
}
public object DeepCopy(object value)
{
return value;
}
public object Replace(object original,
object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return cached;
}
public object Disassemble(object value)
{
return value;
}
/// <summary>
/// The SQL types for the columns
/// mapped by this type.
/// </summary>
public abstract SqlType[] SqlTypes { get; }
}
The repositories for these classes are just a pair of ReadOnlyCollection of the State and Country objects. Again, from the cookbook:
public class States : ReadOnlyCollection<State>
{
// Truncated in the interest of brevity
public static State Arizona = new State("AZ", "Arizona");
public static State Florida = new State("FL", "Florida");
public static State California = new State("CA", "California");
public static State Colorado = new State("CO", "Colorado");
public static State Oklahoma = new State("OK", "Oklahoma");
public static State NewMexico = new State("NM", "New Mexico");
public static State Nevada = new State("NV", "Nevada");
public static State Texas = new State("TX", "Texas");
public static State Utah = new State("UT", "Utah");
public States() : base(new State[]
{
Arizona, Florida, California, Colorado,
Oklahoma, NewMexico, Nevada, Texas, Utah
}
)
{
}
}
Hopefully this helps someone out there.
i couldnt test it, but it should be possible using a convention
public class ComponentConvention : IComponentConvention, IComponentConventionAcceptance
{
public void Accept(IAcceptanceCriteria<IComponentInspector> criteria)
{
criteria.Expect(x => x.Type == typeof(StreetAddress);
}
public void Apply(IComponentInstance instance)
{
instance.Properties.First(p => p.Name == "CountrCode").CustomType<MyUserType>();
}
}
my application has the following database structure:
Transactions:
- TransactionID (PK, Identity)
- Type
- TotalAmount
TransactionDetails:
- TransactionDetailID (PK, Identity)
- TransactionID (PK)
- Amount
ProductTransactions:
- TransactionID (PK, FK)
- Discount
ProductTransactionDetails:
- TransactionDetailID (PK, FK)
- ProductID (FK)
I have this mapped using Fluent NHibernate so that ProductTransaction inherits from Transaction and uses a SubclassMap. I did the same for ProductTransactionDetail and TransactionDetail. I also have a property called "Details" which is a list of TransactionDetail on my Transaction entity with the following mapping:
HasMany(x => x.Details)
.KeyColumn("TransactionID")
.Inverse()
.Cascade.All();
I'd like to be able to override this on my ProductTransaction entity. When using virtual and override the compiler complained but new virtual seemed to work. The problem i have is how i map this since the ProductTransactionDetails doesn't have the TransactionID column in the table. It needs to somehow grab it from the parent table but i'm not sure how to do this.
I'd appreciate it if someone could help fix the issue i'm having or let me know if i'm going about things in the wrong way.
Thanks
Comments are in the code...
Domain Model
public class Product : IEquatable<Product>
{
protected internal virtual int Id { get; set; }
public virtual bool Equals(Product other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
#region Implementation of IEquatable
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof (Product)) return false;
return Equals((Product) obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(Product left, Product right)
{
return Equals(left, right);
}
public static bool operator !=(Product left, Product right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class Transaction : IEquatable<Transaction>
{
private IList<TransactionDetail> details;
// This is declared protected because it is an implementation
// detail that does not belong in the public interface of the
// domain model. It is declared internal so the fluent mapping
// can see it.
protected internal virtual int Id { get; set; }
public virtual double TotalAmount { get; set; }
// This is declared as a IList even though it is recommended
// to use ICollection for a Bag because the the Testing Framework
// passes a HashSet to NHibernate and NHibernate attempts to cast
// it to a List since it is declared a Bag in the mapping.
public virtual IList<TransactionDetail> Details
{
// I lazily initialize the collection because I do not like
// testing for nulls all through my code but you may see
// issues with this if you use Cascade.AllDeleteOrphan in
// the mapping.
get { return details ?? (details = new List<TransactionDetail>()); }
set { details = value; }
}
#region Implementation of IEquatable
// Do not forget to declare this function as virtual or you will
// get a mapping exception saying that this class is not suitable
// for proxying.
public virtual bool Equals(Transaction other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof(Transaction)) return false;
return Equals((Transaction)obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(Transaction left, Transaction right)
{
return Equals(left, right);
}
public static bool operator !=(Transaction left, Transaction right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class TransactionDetail : IEquatable<TransactionDetail>
{
protected internal virtual int Id { get; set; }
public virtual double Amount { get; set; }
#region Implementation of IEquatable
public virtual bool Equals(TransactionDetail other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof (TransactionDetail)) return false;
return Equals((TransactionDetail) obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(TransactionDetail left, TransactionDetail right)
{
return Equals(left, right);
}
public static bool operator !=(TransactionDetail left, TransactionDetail right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class ProductTransaction : Transaction, IEquatable<ProductTransaction>
{
public virtual double Discount { get; set; }
// This is declared 'new' because C# does not support covariant
// return types until v4.0. This requires clients to explicitly
// cast objects of type Transaction to ProductTransaction before
// invoking Details. Another approach would be to change the
// property's name (e.g., ProductDetails) but this also requires
// casting.
public virtual new IList<ProductTransactionDetail> Details
{
get { return base.Details.OfType<ProductTransactionDetail>().ToList(); }
set { base.Details = null == value ? null : value.Cast<TransactionDetail>().ToList(); }
}
#region Implementation of IEquatable
public virtual bool Equals(ProductTransaction other)
{
return base.Equals(other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return Equals(obj as ProductTransaction);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(ProductTransaction left, ProductTransaction right)
{
return Equals(left, right);
}
public static bool operator !=(ProductTransaction left, ProductTransaction right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class ProductTransactionDetail : TransactionDetail, IEquatable<ProductTransactionDetail>
{
public virtual Product Product { get; set; }
#region Implementation of IEquatable
public virtual bool Equals(ProductTransactionDetail other)
{
return base.Equals(other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return Equals(obj as ProductTransactionDetail);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(ProductTransactionDetail left, ProductTransactionDetail right)
{
return Equals(left, right);
}
public static bool operator !=(ProductTransactionDetail left, ProductTransactionDetail right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
Fluent Mapping
internal sealed class ProductMap : ClassMap<Product>
{
internal ProductMap()
{
Table("Product")
;
LazyLoad()
;
Id(x => x.Id)
.Column("ProductId")
.GeneratedBy.Identity()
;
}
}
internal sealed class TransactionMap : ClassMap<Transaction>
{
internal TransactionMap()
{
// The table name is surrounded by back ticks because
// 'Transaction' is a reserved word in SQL. On SQL Server,
// this translates to [Transaction].
Table("`Transaction`")
;
LazyLoad()
;
Id(x => x.Id)
.Column("TransactionId")
.GeneratedBy.Identity()
;
Map(x => x.TotalAmount)
.Column("TotalAmount")
.Not.Nullable()
;
// You should consider treating TransactionDetail as a value
// type that cannot exist outside a Transaction. In this case,
// you should mark the relation as Not.Inverse and save or
// update the transaction after adding a detail instead of
// saving the detail independently.
HasMany(x => x.Details)
.KeyColumn("TransactionID")
.Cascade.All()
.Not.Inverse()
.AsBag()
;
// You have a Type column in your example, which I took to
// mean that you wanted to use the Table Per Hierarchy
// strategy. It this case you need to inform NHibernate
// which column identifies the subtype.
DiscriminateSubClassesOnColumn("Type")
.Not.Nullable()
;
}
}
internal sealed class TransactionDetailMap : ClassMap<TransactionDetail>
{
internal TransactionDetailMap()
{
Table("TransactionDetail")
;
LazyLoad()
;
Id(x => x.Id)
.Column("TransactionDetailId")
.GeneratedBy.Identity()
;
Map(x => x.Amount)
.Column("Amount")
.Not.Nullable()
;
}
}
internal sealed class ProductTransactionMap : SubclassMap<ProductTransaction>
{
internal ProductTransactionMap()
{
KeyColumn("TransactionId")
;
// I recommend giving the discriminator column an explicit
// value for a subclass. Otherwise, NHibernate uses the fully
// qualified name of the class including the namespace. If
// you later move the class to another namespace or rename
// the class then you will have to migrate all of the data
// in your database.
DiscriminatorValue("TransactionKind#product")
;
Map(x => x.Discount)
.Column("Discount")
.Nullable()
;
// Do not map the over-ridden version of
// the Details property. It is handled
// by the base class mapping.
}
}
internal sealed class ProductTransactionDetailMap : SubclassMap<ProductTransactionDetail>
{
internal ProductTransactionDetailMap()
{
// There was no Type column in your example for this table,
// whcih I took to mean that you wished to use a Table Per
// Class strategy. In this case, you need to provide the
// table name even though it is a subclass.
Table("ProductTransactionDetail")
;
KeyColumn("TransactionDetailId")
;
References(x => x.Product)
.Column("ProductId")
.Not.Nullable()
;
}
}
Unit Tests
[TestClass]
public class UnitTest1
{
private static ISessionFactory sessionFactory;
private static Configuration configuration;
[TestMethod]
public void CanCorrectlyMapTransaction()
{
using (var dbsession = OpenDBSession())
{
var product = new Product();
dbsession.Save(product);
new PersistenceSpecification<Transaction>(dbsession)
.CheckProperty(t => t.TotalAmount, 100.0)
.CheckComponentList(
t => t.Details,
new[] {
new TransactionDetail {
Amount = 75.0,
},
new ProductTransactionDetail {
Amount = 25.0,
Product = product,
},
}
)
.VerifyTheMappings()
;
}
}
private static Configuration Configuration
{
get
{
return configuration ?? (
configuration = forSQLite().Mappings(
m => m.FluentMappings
.Conventions.Setup(x => x.Add(AutoImport.Never()))
.Add(typeof(ProductMap))
.Add(typeof(ProductTransactionMap))
.Add(typeof(ProductTransactionDetailMap))
.Add(typeof(TransactionMap))
.Add(typeof(TransactionDetailMap))
)
.BuildConfiguration()
);
}
}
private static ISessionFactory SessionFactory
{
get { return sessionFactory ?? (sessionFactory = Configuration.BuildSessionFactory()); }
}
private static ISession OpenDBSession()
{
var session = SessionFactory.OpenSession();
// Ideally, this would be done once on the database
// session but that does not work when using SQLite as
// an in-memory database. It works in all other cases.
new SchemaExport(configuration)
.Execute(
true, // echo schema to Console
true, // create schema on connection
false, // just drop do not create
session.Connection, // an active database connection
null // writer for capturing schema
);
return session;
}
private static FluentConfiguration forSQLite()
{
return Fluently.Configure()
.Database(
SQLiteConfiguration
.Standard
.InMemory()
.ShowSql()
);
}
}