I am using the service / repository / unit of work design patterns in an application. Thus far this has always worked very well and allows me to separate my logic from POCO classes and repositories. Recently I tried to use the same pattern for use with an oracle database so I set up as follows:
public class Repository<T> : IRepository<T> where T : class, new()
{
// private properties
private readonly OracleCommand _command;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="command"></param>
/// <param name="connection"></param>
public Repository(OracleCommand command)
{
this._command = command;
}
/// <summary>
/// Returns the datareader for the stored procedure
/// </summary>
/// <param name="storedProcedureName">The name of the SPROC to execute</param>
/// <param name="parameters">The parameters needed for the SPROC</param>
/// <returns></returns>
public async Task<IDataReader> ExecuteReaderAsync(string storedProcedureName, IList<OracleParameter> parameters = null)
{
// Set up our command
this.InitiateCommand(storedProcedureName, parameters.ToArray());
// Return our data reader
return await this._command.ExecuteReaderAsync();
}
/// <summary>
/// Create, updates or deletes an entity
/// </summary>
/// <param name="storedProcedureName">The name of the SPROC to execute</param>
/// <param name="parameters">The parameters needed for the SPROC</param>
public async Task CreateUpdateOrDeleteAsync(string storedProcedureName, IList<OracleParameter> parameters = null)
{
// Set up our command
this.InitiateCommand(storedProcedureName, parameters.ToArray());
// Execute our command
await this._command.ExecuteNonQueryAsync();
}
/// <summary>
/// Intiates the command object
/// </summary>
/// <param name="storedProcedureName">The name of the SPROC to execute</param>
/// <param name="parameters">An array of parameters</param>
private void InitiateCommand(string storedProcedureName, OracleParameter[] parameters)
{
// Set up the command object
this._command.CommandText = storedProcedureName;
this._command.Parameters.Clear();
// If we have any parameters
if (parameters != null)
{
// Assign each parameter to the command object
this._command.Parameters.AddRange(parameters);
}
}
}
The unit of work looked like this:
public class OracleUnitOfWork : IOracleUnitOfWork
{
// Private properties
private readonly OracleConnection _connection;
private readonly OracleCommand _command;
private readonly Dictionary<Type, object> _repositories;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="connectionStringName">The connection string name to use</param>
public OracleUnitOfWork(string connectionStringName = "OracleConnection")
{
// Create instances for our private properties
this._repositories = new Dictionary<Type, object>();
this._connection = new OracleConnection(ConfigurationManager.ConnectionStrings[connectionStringName].ToString());
this._command = new OracleCommand();
// Set our command up
this._command.Connection = this._connection;
this._command.CommandType = CommandType.StoredProcedure;
// Open our connection
this._connection.Open();
}
/// <summary>
/// Gets the entity repository
/// </summary>
/// <typeparam name="T">The entity model</typeparam>
/// <returns></returns>
public IRepository<T> GetRepository<T>() where T : class, new()
{
// If our repositories have a matching repository, return it
if (_repositories.Keys.Contains(typeof(T)))
return _repositories[typeof(T)] as IRepository<T>;
// Create a new repository for our entity
var repository = new Repository<T>(this._command);
// Add to our list of repositories
_repositories.Add(typeof(T), repository);
// Return our repository
return repository;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes of any attached resources
/// </summary>
/// <param name="disposing">A boolean indicating whether the object is being disposed</param>
protected virtual void Dispose(bool disposing)
{
// If we are disposing
if (disposing)
{
// Close our connection
this._connection.Close();
this._connection.Dispose();
this._command.Dispose();
}
}
}
So far so good, I then set up an abstract service like this:
public abstract class OracleService<T> where T : class, new()
{
// Properties
public IRepository<T> Repository { get; private set; }
private readonly IOracleUnitOfWork _oracleUnitOfWork;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="oracleUnitOfWork">The Oracle UnitOfWork class</param>
public OracleService(IOracleUnitOfWork oracleUnitOfWork)
{
// If not unit of work has been passed, throw an error
ThrowIf.ArgumentIsNull(() => oracleUnitOfWork);
// Add our references
this._oracleUnitOfWork = oracleUnitOfWork;
this.Repository = oracleUnitOfWork.GetRepository<T>();
}
}
so then a typical service would look like this:
public class ProductService : OracleService<Product>
{
/// <summary>
/// Default Constructor
/// </summary>
/// <param name="oracleUnitOfWork">The Unit Of Work class for handling connections</param>
public ProductService(IOracleUnitOfWork oracleUnitOfWork)
: base(oracleUnitOfWork)
{
}
/// <summary>
/// Get a single Product entity
/// </summary>
/// <param name="productCode"></param>
public async Task<Product> GetAsync(string productCode)
{
// Create our parameters
var parameters = new List<OracleParameter>();
parameters.Add(new OracleParameter("P_CUR", OracleDbType.RefCursor, ParameterDirection.Output));
parameters.Add(new OracleParameter("P_PRODCODE", productCode));
// Get our reader and return our model
using (var reader = await Repository.ExecuteReaderAsync("ibpa_products.readProduct", parameters))
while (reader.Read())
return OracleMapper.Product.Map(reader);
// Fallback
return null;
}
/// <summary>
/// Get a single Product entity
/// </summary>
/// <param name="search">Search criteria</param>
/// <param name="accountNumber">The account number to search</param>
/// <returns>Product Object</returns>
public async Task<Product> GetAsync(string search, string accountNumber)
{
// Create our parameters
var parameters = new List<OracleParameter>();
parameters.Add(new OracleParameter("P_SEARCH", search));
parameters.Add(new OracleParameter("P_ACCOUNT", accountNumber));
// Get our reader and return our model
using (var reader = await Repository.ExecuteReaderAsync("ibpa_products.productSearch", parameters))
while (reader.Read())
return OracleMapper.Product.Map(reader);
// Fallback
return null;
}
}
When it is simple like that, everything is fine. But I have a need now to combine a few services. So I created a provider which looks something like this:
public class StockServiceProvider
{
private readonly StockService _stockService;
private readonly OrderServiceProvider _orderServiceProvider;
private readonly ProductServiceProvider _productServiceProvider;
private readonly ITroposUnitOfWork _troposUnitOfWork;
/// <summary>
/// Default constructor of the StockServiceProvider
/// </summary>
/// <param name="oracleUnitOfWork">The Unit Of Work class for handling connections</param>
public StockServiceProvider(IOracleUnitOfWork oracleUnitOfWork, connectionType connectionType)
{
_stockService = new StockService(oracleUnitOfWork);
_orderServiceProvider = new OrderServiceProvider(oracleUnitOfWork, _troposUnitOfWork, connectionType);
_orderServiceProvider.OrderService.OrderLineService = new OrderLineService(oracleUnitOfWork);
_productServiceProvider = new ProductServiceProvider(oracleUnitOfWork, connectionType);
}
/// <summary>
/// Get a stock object async
/// </summary>
/// <param name="lotNumber">Lot number to fetch</param>
/// <param name="includes">List of string paramaters to include with account object</param>
/// <returns>Account Object</returns>
public async Task<Stock> GetAsync(string lotNumber, params string[] includes)
{
// Get our model
var model = await this._stockService.GetAsync(lotNumber);
// If we don't find the model, throw an error
if (model == null)
throw new NullReferenceException($"{ lotNumber } does not exist in the stock table.");
// Fake eager load
await EagerLoader.LoadAsync(SetPropertyValue, model, includes);
// Return our full model
return model;
}
/// <summary>
/// Sets the property value async
/// </summary>
/// <param name="model">The parent model</param>
/// <param name="property">The current property</param>
/// <param name="includes">List of params to include</param>
private async Task SetPropertyValue(Stock model, PropertyInfo property, string[] includes)
{
// Switch on the property name
switch (property.Name)
{
case "AllocatedItems":
// Add allocated items to our model
var orders = await _orderServiceProvider.OrderService.ListAllocatedAsync(model.LotNumber);
//Assign the orders to allocated items.
model.AllocatedItems = orders;
//Exit
break;
case "RelatedItems":
// Add the related items to the
var relatedItems = await _stockService.ListAsync(model.LotNumber);
model.RelatedItems = relatedItems.ToList();
//Exit
break;
case "Product":
// Add the product to the stock model.
var product = await _productServiceProvider.GetAsync(model.ProductCode);
model.Product = product;
//Exit
break;
default:
// Do nothing
break;
}
}
}
There are a few issues I have with this.
- The services / providers are tightly bound because I am not injecting their interfaces into the constructor, but I am doing this on purpose because there is potential to be a lot of interfaces that would need to be injected
- I used the UserManager in Identity.Framework as an example and tried to do something similar in my services; i.e. it has public properties that are other "Managers" like the RoleManager. This is assigned whenever a role should be modified. The problem I have with this method is that my "providers" are where the logic happens so if I have multiple services instantiated in the constructor, it makes sense to assign the properties, but this negates the need of the properties because they will always be set...
Does anyone know of a design pattern or can help me better handle these problems?
Aucun commentaire:
Enregistrer un commentaire