Good progress on CSLA6 using DI and FactoryObjects - but troublesome service registration #2684
-
Happy Friday! I wanted to give some feedback as I follow Rocky's advice given on #2670 for how to use DI to create factories while upgrading to CSLA 6. I'm starting to feel better and make good progress on what many of us will find scary at first (I see the same concern in many discussion posts). Some of us have many business objects that have been around since v3 or previous so it can be overwhelming. I have two questions at the bottom. Anyway, I create factories so that I can:
Based on your suggestion, here is where I'm at. I created a solution beside my existing one and starting adding upgraded projects from the bottom up:
Here is an example of my new base classes for ReadOnlyList public abstract class JTReadOnlyListBaseFactory<T, C>
where T : JTReadOnlyListBase<T, C>, new()
where C : JTReadOnlyBase<C>, new()
{
protected IDataPortal<T> Portal { get; set; }
protected ApplicationContext ApplicationContext { get; set; }
/// <summary>
/// Csla friendly way to create a new object. Updates to Csla5 added code analyzer checks that no objects have any
/// calls directly to constructors. Forces objects to go through the DataPortal/ObjectFactory and properly manages object state
/// </summary>
/// <returns>an empty object of this type</returns>
public T CslaSafeConstructor()
{
return Portal.Fetch(new BaseOnlyEmptySemaphoreObject());
}
}
[Serializable()]
public abstract class JTReadOnlyListBase<T, C> : ReadOnlyListBase<T, C>
where T : JTReadOnlyListBase<T, C>
where C : Csla.Core.IBusinessObject
{
/// <summary>
/// Corresponds to factory CslaSafeConstructor above
/// </summary>
/// <param name="semaphoreObject"></param>
[RunLocal]
[Fetch]
private void DP_Fetch(BaseOnlyEmptySemaphoreObject semaphoreObject)
{
} Here is an example of a concrete class that implements the base classes public class TranslationRecordInfoListFactory : BusinessBaseClasses.JTReadOnlyListBaseFactory<TranslationRecordInfoList, TranslationRecordInfo>
{
#region DI Constructor and Factory Methods
public TranslationRecordInfoListFactory(IDataPortal<TranslationRecordInfoList> dataPortal, ApplicationContext applicationContext)
{
Portal = dataPortal;
ApplicationContext = applicationContext;
}
public TranslationRecordInfoList GetAll()
{
RelationPredicateBucket filter = new RelationPredicateBucket();
//filter.Relations.Add(TranslationEntity.Relations.SOME_RELATION);
//filter.PredicateExpression.Add(TranslationFields.FILTER_COLUMN == FUNCTION_PARAMETER);
SortExpression sortExp = new SortExpression(new SortClause(TranslationFields.Description, null, SortOperator.Ascending));
return Portal.Fetch(new LLBLGenDataUtilities.Criteria.LLBLGenFilterCriteria(filter, sortExp));
}
internal TranslationRecordInfoList GetEmpty()
{
return CslaSafeConstructor();
}
#endregion
}
/// <summary>
/// DAL entity: <see cref="JusticeTrax.Common.DAL.EntityClasses.TranslationEntity" />
/// </summary>
[Serializable()]
public class TranslationRecordInfoList : BusinessBaseClasses.JTReadOnlyListBase<TranslationRecordInfoList, TranslationRecordInfo>
{
public TranslationRecordInfoList()
{
}
#region Data Access
[Fetch]
private void DP_Fetch(
LLBLGenDataUtilities.Criteria.LLBLGenFilterCriteria criteria,
[Inject] LanguageInfoListFactory languageInfoListFactory,
[Inject] TranslationRecordInfoFactory translationInfoFactory,
[Inject] LLBLGenDataUtilities.JTDataAdapterManagerFactory dataAdapterFactory)
{
//can connect to db safely (ensuring only one db connection using injected scoped (dataAdapterFactory)
//make calls to "outside" business objects using additional injected factories (languageInfoListFactory)
//make calls to static "Load" methods of child objects that fill this list (translationInfoFactory)
} You can see I am able, with just a few lines of code cut and pasted above every business object, to create a corresponding factory. Then it is easy to cut the static factory methods out of the business object and paste them into the factory object above. Then it is easy to:
So far so good. I'll be honest I was really really overwhelmed about the thought of having to create a factory to keep my nice helpful methods for getting business objects with all kinds of insider parameters that I don't want my user's having to know about. But through cut/paste of new factory objects, I could probably have all 200 to 300+ business objects done in a day. Now my UI and automated test projects can easily (either through DI or ServiceProvider.GetService) fire up a factory and then call the fetch/create methods like before. There is only one pain point that is just too onerous and won't be scale-able with as many classes as I have.
The only way to get past that is to hand register the DataPortal for each T that my factory is for. So for a simple 4 business objects I end up with the following 8 lines needed in my blazor app's Program.cs //These are required for the xxxFactory objects to be created via DI
builder.Services.AddScoped<Localization.LanguageInfoFactory>();
builder.Services.AddScoped<Localization.LanguageInfoListFactory>();
builder.Services.AddScoped<Localization.TranslationRecordInfoFactory>();
builder.Services.AddScoped<Localization.TranslationRecordInfoListFactory>();
//These are required to be present by framework (as it chains through the above xxxFactory objects discovering/resolving more services)
builder.Services.AddScoped<DataPortal<Localization.LanguageInfo>>();
builder.Services.AddScoped<DataPortal<Localization.LanguageInfoList>>();
builder.Services.AddScoped<DataPortal<Localization.TranslationRecordInfo>>();
builder.Services.AddScoped<DataPortal<Localization.TranslationRecordInfoList>>(); That will get nuts with 600 or so lines requires for 300 or so business objects! Question
I'm thinking I won't be the last guy that will bump into this, so I'd be happy to share the code if I can come up with a way to do this and save having to manually have that much configuration code in every client application (of which we have 5+) |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 4 replies
-
Question 1 I haven't shared that code yet because I'm not sure it's production grade yet, but I probably will do so at some point. It uses reflection at app startup. I did it that way because I want to define the rules for what gets registered, to reduce the potential for it introducing a security vulnerability. Randomly registering any old type from any old assembly might not be the best choice from a security perspective! The specific job you are after can probably also be done using a source generator, but I haven't tried that yet. Source generators are fairly time consuming to get right. Apart from anything else, they are quite difficult to debug. I would suggest starting with reflection to learn what you want, and progressing to a source generator when you have the time and inclination to do so. In this case, the registration task only happens once, at startup, so the runtime performance benefit to be gained from avoiding reflection is pretty small, in my opinion. To be clear though, this problem isn't really a Csla problem but a more general DI problem, so in my opinion the solution need not be in Csla - and indeed it might actually be better if it wasn't in Csla. One of the teachings we might take from the recent Log4j vulnerability is to think carefully before extending a library's purpose with a nice-to-have extension feature that could otherwise be created - and maintained - separately. A separate library needs to be imported only by those people who want to use it, who trust it, and who are prepared to help maintain it, reducing the number of systems that become vulnerable if an attack vector is ever detected. |
Beta Was this translation helpful? Give feedback.
-
Here's a sample of how I did the discovery of the types to register. I'm not absolutely sure this will make sense without the rest of the code, but in case it's helpful, here is a mind dump! No warranty expressed or implied, blah blah ;-) /// <summary>
/// Perform the all of the type filtering that has been defined
/// to discover the implementing types that will be registered
/// </summary>
/// <returns>A set of types that match the filters defined</returns>
private IEnumerable<Type> GetMatchingImplementingTypes()
{
IEnumerable<Type> allTypes;
List<Type> matchingTypes = new List<Type>();
allTypes = _assembly.GetTypes();
// Test each type from the assembly to see if they match our filters
foreach (Type typeUnderTest in allTypes)
{
// Perform exclusions
if (typeUnderTest.IsAbstract) continue;
if (typeUnderTest.IsInterface) continue;
// TODO: Add your filter for types of interest here; for example, you might use the IsAssignableFrom method
if (typeof(BusinessBase).IsAssignableFrom(typeUnderTest))
{
matchingTypes.Add(typeUnderTest);
}
}
return matchingTypes.AsEnumerable();
} Disclaimer: It is possible that I am using the IsAssignableFrom method incorrectly here. I think it would work as shown, but that's not the test I am actually using in my code, so I can't guarantee that line of code in this sample. Performing the actual DI registration using an instance of the Type class is as simple as: services.AddTransient(serviceType, implementingType); Or, if you don't want to register a type against an interface, it's just: services.AddTransient(implementingType); Obviously you can vary the scope by changing the extension method on IServiceCollection you use - AddTransient, AddScoped or AddSingleton. Add a foreach and you should be good to go. |
Beta Was this translation helpful? Give feedback.
-
Will do.
…On Wed, Feb 16, 2022 at 4:03 PM TheCakeMonster ***@***.***> wrote:
We should move questions on that off the CSLA repo, I suspect.
The DiscoverTypesIn is the starting point for finding types in another
assembly if you really need to - but generally I don't. Instead, I have an
extension method in a static class called ServiceCollectionExtensions
within whatever assembly I am targeting, as the extension method can be
anywhere.
Having said that, if you only want to register four well known types, I
don't see that it's worth any of it.
If you want to ask more questions, maybe start a discussion in the
DependencyInjection repo.
—
Reply to this email directly, view it on GitHub
<#2684 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABUM5P4475GVDRO53WR2T6LU3QUMTANCNFSM5LP6HFXA>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
Question 1
I have already written code to do inspection and auto-registration of types in an assembly, so yes, it is very possible to do so. I wrote it to do fairly generic convention-over-configuration type registrations, such as registering MyTypeRepository as the implementation of IMyTypeRepository by finding all of the classes with names that ended with the word 'Repository' from a specific assembly.
I haven't shared that code yet because I'm not sure it's production grade yet, but I probably will do so at some point. It uses reflection at app startup. I did it that way because I want to define the rules for what gets registered, to reduce the potential for it introducing a security v…